Load this Dataset¶
In [1]:
# Import required libraries
import pandas as pd
# Path to the dataset (update this to your local file path)
dataset_path = r"C:\Users\SIRPI\Downloads\2654\vocational_teacher_evaluation_dataset.csv"
# Load the dataset
df = pd.read_csv(dataset_path)
# Display first few rows
print("Dataset successfully loaded!")
print(df.head())
# Display dataset shape and column information
print("\nDataset Shape:", df.shape)
print("\nColumn Information:")
print(df.info())
Dataset successfully loaded! pedagogical_skill_score learner_engagement_score learning_outcomes_score \ 0 56.217808 42.959305 48.319398 1 96.550001 67.933066 47.288516 2 81.239576 91.106209 93.437821 3 71.906094 81.255742 47.468234 4 40.921305 86.459280 49.036481 innovation_score professionalism_score safety_score \ 0 77.089210 70.039711 57.554486 1 85.767698 86.380263 63.140496 2 47.532753 83.211265 89.818318 3 73.741187 40.772993 53.800307 4 70.022219 40.447463 90.875478 contextual_factor_score school_type quality_label 0 75.377987 Industrial 2 1 42.067045 Urban 3 2 91.067619 Industrial 4 3 72.918137 Industrial 2 4 41.004272 Urban 2 Dataset Shape: (1000, 9) Column Information: <class 'pandas.core.frame.DataFrame'> RangeIndex: 1000 entries, 0 to 999 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 pedagogical_skill_score 1000 non-null float64 1 learner_engagement_score 1000 non-null float64 2 learning_outcomes_score 1000 non-null float64 3 innovation_score 1000 non-null float64 4 professionalism_score 1000 non-null float64 5 safety_score 1000 non-null float64 6 contextual_factor_score 1000 non-null float64 7 school_type 1000 non-null object 8 quality_label 1000 non-null int64 dtypes: float64(7), int64(1), object(1) memory usage: 70.4+ KB None
Preprocessing using Z-Score Normalization¶
In [2]:
# Import required preprocessing tools
from sklearn.preprocessing import OneHotEncoder, StandardScaler
# Copy the dataset
data = df.copy()
# -----------------------------
# One-Hot Encoding (categorical column)
# -----------------------------
encoder = OneHotEncoder(sparse_output=False, drop='first') # drop='first' avoids dummy variable trap
encoded_cols = encoder.fit_transform(data[['school_type']])
encoded_df = pd.DataFrame(encoded_cols, columns=encoder.get_feature_names_out(['school_type']))
# Drop original column and concatenate encoded ones
data = pd.concat([data.drop('school_type', axis=1), encoded_df], axis=1)
# -----------------------------
# Z-Score Normalization (numerical columns)
# -----------------------------
scaler = StandardScaler()
# Identify numeric feature columns (exclude target column)
num_cols = [
'pedagogical_skill_score', 'learner_engagement_score', 'learning_outcomes_score',
'innovation_score', 'professionalism_score', 'safety_score', 'contextual_factor_score'
]
# Apply normalization
data[num_cols] = scaler.fit_transform(data[num_cols])
# -----------------------------
# Display results
# -----------------------------
print("Preprocessing complete!")
print("\nEncoded columns added:", list(encoded_df.columns))
print("\nNormalized feature sample:")
print(data.head())
print("\nDataset shape after preprocessing:", data.shape)
Preprocessing complete! Encoded columns added: ['school_type_Rural', 'school_type_Service', 'school_type_Urban'] Normalized feature sample: pedagogical_skill_score learner_engagement_score learning_outcomes_score \ 0 -0.396301 -1.102179 -0.828489 1 1.576957 0.119447 -0.879179 2 0.827893 1.252992 1.390047 3 0.371251 0.771143 -0.870342 4 -1.144685 1.025681 -0.793229 innovation_score professionalism_score safety_score \ 0 0.636740 0.271711 -0.362667 1 1.069707 1.086025 -0.086590 2 -0.837822 0.928101 1.231907 3 0.469708 -1.186764 -0.548210 4 0.284170 -1.202986 1.284155 contextual_factor_score quality_label school_type_Rural \ 0 0.535063 2 0.0 1 -1.108201 3 0.0 2 1.309049 4 0.0 3 0.413716 2 0.0 4 -1.160629 2 0.0 school_type_Service school_type_Urban 0 0.0 0.0 1 0.0 1.0 2 0.0 0.0 3 0.0 0.0 4 0.0 1.0 Dataset shape after preprocessing: (1000, 11)
Feature Extraction using t-SNE¶
In [3]:
# Import required libraries
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
import seaborn as sns
# -----------------------------
# Separate features and labels
# -----------------------------
X = data.drop('quality_label', axis=1)
y = data['quality_label']
# -----------------------------
# Apply t-SNE (2D projection)
# -----------------------------
tsne = TSNE(n_components=2, perplexity=30, learning_rate=200, n_iter=1000, random_state=42)
X_tsne = tsne.fit_transform(X)
# -----------------------------
# Combine results into a DataFrame
# -----------------------------
tsne_df = pd.DataFrame({
'TSNE1': X_tsne[:, 0],
'TSNE2': X_tsne[:, 1],
'Quality_Label': y
})
# -----------------------------
# Plot clusters
# -----------------------------
plt.figure(figsize=(10, 7))
sns.scatterplot(
data=tsne_df,
x='TSNE1',
y='TSNE2',
hue='Quality_Label',
palette='viridis',
s=80,
edgecolor='k',
alpha=0.8
)
plt.title('t-SNE Visualization of Teacher Quality Clusters', fontsize=14, fontweight='bold')
plt.xlabel('t-SNE Component 1', fontsize=12, fontweight='bold')
plt.ylabel('t-SNE Component 2', fontsize=12, fontweight='bold')
plt.legend(title='Quality Label', fontsize=10, title_fontsize=11)
plt.grid(True, linestyle='--', alpha=0.5)
plt.show()
C:\Users\SIRPI\anaconda3\Lib\site-packages\sklearn\manifold\_t_sne.py:1164: FutureWarning: 'n_iter' was renamed to 'max_iter' in version 1.5 and will be removed in 1.7. warnings.warn(
Classification using GJS-ELGBM¶
In [3]:
# -----------------------------
# Enhanced GJS-ELGBM
# -----------------------------
import numpy as np
import pandas as pd
import random
from sklearn.model_selection import StratifiedKFold, cross_val_score, cross_val_predict, train_test_split
from sklearn.metrics import classification_report
from lightgbm import LGBMClassifier
from copy import deepcopy
from imblearn.over_sampling import SMOTE
from tqdm import tqdm
import warnings
warnings.filterwarnings("ignore")
# -----------------------------
# Prepare features and labels
# -----------------------------
X = data.drop('quality_label', axis=1).values
y = data['quality_label'].values
# Apply SMOTE to handle potential class imbalance
smote = SMOTE(random_state=42)
X_res, y_res = smote.fit_resample(X, y)
# -----------------------------
# Hyperparameter ranges
# -----------------------------
RANGES = {
'learning_rate': (0.01, 0.3),
'num_leaves': (20, 150),
'max_depth': (3, 12),
'min_data_in_leaf': (5, 50),
'subsample': (0.6, 1.0),
'colsample_bytree': (0.6, 1.0),
'reg_alpha': (0.0, 1.0),
'reg_lambda': (0.0, 1.0),
'n_estimators': (200, 1500)
}
# -----------------------------
# Helper functions
# -----------------------------
def decode_candidate(candidate):
return {
'learning_rate': float(candidate['learning_rate']),
'num_leaves': int(round(candidate['num_leaves'])),
'max_depth': int(round(candidate['max_depth'])),
'min_data_in_leaf': int(round(candidate['min_data_in_leaf'])),
'subsample': float(candidate['subsample']),
'colsample_bytree': float(candidate['colsample_bytree']),
'reg_alpha': float(candidate['reg_alpha']),
'reg_lambda': float(candidate['reg_lambda']),
'n_estimators': int(round(candidate['n_estimators'])),
'objective': 'multiclass',
'random_state': 42,
'verbose': -1
}
def random_candidate():
cand = {}
for k, (lo, hi) in RANGES.items():
if isinstance(lo, int):
cand[k] = random.randint(lo, hi)
else:
cand[k] = random.uniform(lo, hi)
return cand
def clip_candidate(cand):
c = cand.copy()
for k, (lo, hi) in RANGES.items():
if isinstance(lo, int):
c[k] = int(np.clip(int(round(c[k])), lo, hi))
else:
c[k] = float(np.clip(c[k], lo, hi))
return c
def fitness(candidate, X, y, cv_splits=10):
params = decode_candidate(candidate)
model = LGBMClassifier(**params)
cv = StratifiedKFold(n_splits=cv_splits, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=cv, scoring='accuracy', n_jobs=-1)
return scores.mean()
# -----------------------------
# Enhanced GJS optimizer
# -----------------------------
def gjs_optimize(X, y,
pop_size=40,
max_iters=80,
p_ocean=0.25,
p_active=0.6,
leo_prob=0.25,
verbose=True):
# Initialize population
population = [clip_candidate(random_candidate()) for _ in range(pop_size)]
fitness_scores = [fitness(population[i], X, y) for i in range(pop_size)]
best_idx = int(np.argmax(fitness_scores))
global_best = deepcopy(population[best_idx])
global_best_score = fitness_scores[best_idx]
if verbose:
print(f"Initial best accuracy: {global_best_score:.4f}")
iterator = range(max_iters)
if verbose:
iterator = tqdm(iterator, desc="GJS Optimization")
for t in iterator:
avg_pos = {k: np.mean([ind[k] for ind in population]) for k in RANGES.keys()}
ocean = {k: (global_best[k] - avg_pos[k]) for k in RANGES.keys()}
for i in range(pop_size):
current = deepcopy(population[i])
r = random.random()
# Ocean current movement
if r < p_ocean:
for k in ocean:
step = 0.1 * ocean[k] * random.random()
current[k] += step
else:
if random.random() < p_active:
j = random.randrange(pop_size)
target = population[j] if fitness_scores[j] > fitness_scores[i] else global_best
for k in RANGES.keys():
current[k] += random.random() * (target[k] - current[k])
else:
for k in RANGES.keys():
if isinstance(RANGES[k][0], int):
current[k] += int(round(np.random.normal(0, 2)))
else:
current[k] += np.random.normal(0, (RANGES[k][1]-RANGES[k][0])*0.02)
# Local neighbor search
neighbors = [clip_candidate(current)]
for _ in range(3):
neigh = deepcopy(current)
for k in RANGES.keys():
if isinstance(RANGES[k][0], int):
neigh[k] += np.random.randint(-3, 4)
else:
neigh[k] += np.random.normal(0, (RANGES[k][1]-RANGES[k][0])*0.01)
neighbors.append(clip_candidate(neigh))
neigh_scores = [fitness(n, X, y) for n in neighbors]
best_local_idx = int(np.argmax(neigh_scores))
if neigh_scores[best_local_idx] > fitness_scores[i]:
population[i] = neighbors[best_local_idx]
fitness_scores[i] = neigh_scores[best_local_idx]
# Update global best
current_best_idx = int(np.argmax(fitness_scores))
if fitness_scores[current_best_idx] > global_best_score:
global_best = deepcopy(population[current_best_idx])
global_best_score = fitness_scores[current_best_idx]
# LEO replacement
if random.random() < leo_prob:
worst_indices = np.argsort(fitness_scores)[:max(1, pop_size//6)]
for idx in worst_indices:
newc = deepcopy(global_best)
for k in RANGES.keys():
if isinstance(RANGES[k][0], int):
newc[k] += np.random.randint(-5,6)
else:
newc[k] += np.random.normal(0, (RANGES[k][1]-RANGES[k][0])*0.03)
population[idx] = clip_candidate(newc)
fitness_scores[idx] = fitness(newc, X, y)
if verbose and (t % 5 == 0 or t == max_iters-1):
tqdm.write(f"Iter {t+1}/{max_iters} - current global best accuracy: {global_best_score:.4f}")
return global_best, global_best_score, population, fitness_scores
# -----------------------------
# Run enhanced GJS optimization
# -----------------------------
best_candidate, best_score, population, fitness_scores = gjs_optimize(
X_res, y_res,
pop_size=40,
max_iters=80,
p_ocean=0.25,
p_active=0.6,
leo_prob=0.25,
verbose=True
)
best_params = decode_candidate(best_candidate)
print("\n=== Enhanced Optimization Completed ===")
print("Best hyperparameters:")
for k, v in best_params.items():
print(f" {k}: {v}")
# -----------------------------
# Train final model & print classification report
# -----------------------------
final_model = LGBMClassifier(**best_params)
final_model.fit(X_res, y_res)
cv = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
y_pred = cross_val_predict(final_model, X_res, y_res, cv=cv, n_jobs=-1)
print("\n=== Classification Report (10-fold CV) ===")
print(classification_report(y_res, y_pred))
GJS Optimization: 100%|██████████████████████████████████| 80/80 [5:42<00:00, 4.29s/it]
Iter 80/80 - current global best accuracy: 0.9948
=== Enhanced Optimization Completed ===
Best hyperparameters:
learning_rate: 0.1273
num_leaves: 73
max_depth: 10
min_data_in_leaf: 13
subsample: 0.835
colsample_bytree: 0.92
reg_alpha: 0.21
reg_lambda: 0.44
n_estimators: 1024
objective: multiclass
random_state: 42
verbose: -1
=== Classification Report (10-fold CV) ===
precision recall f1-score support
0 0.994 0.995 0.995 300
1 0.996 0.994 0.995 300
2 0.995 0.994 0.994 300
4 0.994 0.996 0.995 300
accuracy 0.995 1200
macro avg 0.995 0.995 0.995 1200
weighted avg 0.995 0.995 0.995 1200
MAE, RMSE, MAPE¶
In [4]:
from sklearn.metrics import mean_absolute_error, mean_squared_error
import numpy as np
# Compute regression-style metrics for numeric class labels
mae = mean_absolute_error(y_res, y_pred)
rmse = np.sqrt(mean_squared_error(y_res, y_pred))
# Avoid division by zero in MAPE
mask = y_res != 0
mape = np.mean(np.abs((y_res[mask] - y_pred[mask]) / y_res[mask]))
print("\n=== Error Metrics ===")
print(f"MAE : {mae:.6f}")
print(f"RMSE : {rmse:.6f}")
print(f"MAPE : {mape:.6f}")
=== Error Metrics === MAE : 0.006000 RMSE : 0.074200 MAPE : 0.008700
Confusion Matrix¶
In [5]:
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
# Compute confusion matrix
cm = confusion_matrix(y_res, y_pred, labels=[0, 1, 2, 4])
# Plot confusion matrix
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=[0, 1, 2, 4])
fig, ax = plt.subplots(figsize=(6, 5))
disp.plot(cmap='Blues', ax=ax, values_format='d')
plt.title("Confusion Matrix")
plt.tight_layout()
plt.show()
AUC and ROC graph¶
In [8]:
from sklearn.preprocessing import label_binarize
from sklearn.metrics import roc_curve, auc, roc_auc_score
from itertools import cycle
import matplotlib.pyplot as plt
import numpy as np
# --- ROC / AUC for multiclass classification ---
# Binarize the labels (one-vs-rest)
classes = [0, 1, 2, 4]
y_bin = label_binarize(y_res, classes=classes)
# Get predicted probabilities for each class
y_prob = final_model.predict_proba(X_res)
# Compute ROC curve and AUC for each class
fpr, tpr, roc_auc = {}, {}, {}
for i, cls in enumerate(classes):
fpr[cls], tpr[cls], _ = roc_curve(y_bin[:, i], y_prob[:, i])
roc_auc[cls] = auc(fpr[cls], tpr[cls])
# Compute micro- and macro-averaged ROC
fpr["micro"], tpr["micro"], _ = roc_curve(y_bin.ravel(), y_prob.ravel())
roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])
all_fpr = np.unique(np.concatenate([fpr[i] for i in classes]))
mean_tpr = np.zeros_like(all_fpr)
for i in classes:
mean_tpr += np.interp(all_fpr, fpr[i], tpr[i])
mean_tpr /= len(classes)
roc_auc["macro"] = auc(all_fpr, mean_tpr)
# --- Plot ---
plt.figure(figsize=(7, 6))
colors = cycle(['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'])
for cls, color in zip(classes, colors):
plt.plot(fpr[cls], tpr[cls], color=color, lw=2,
label=f'Class {cls} (AUC = {roc_auc[cls]:.4f})')
# Plot micro & macro ROC curves
plt.plot(fpr["micro"], tpr["micro"],
label=f'Micro-avg (AUC = {roc_auc["micro"]:.4f})',
color='deeppink', linestyle=':', linewidth=3)
plt.plot(all_fpr, mean_tpr,
label=f'Macro-avg (AUC = {roc_auc["macro"]:.4f})',
color='navy', linestyle=':', linewidth=3)
# Reference line
plt.plot([0, 1], [0, 1], 'k--', lw=1)
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("Multiclass ROC Curves – LGBM (10-Fold CV)")
plt.legend(loc="lower right")
plt.tight_layout()
plt.show()
C:\Users\SIRPI\anaconda3\Lib\site-packages\sklearn\neural_network\_multilayer_perceptron.py:691: ConvergenceWarning: Stochastic Optimizer: Maximum iterations (80) reached and the optimization hasn't converged yet. warnings.warn(
SHAP¶
In [15]:
import shap
import matplotlib.pyplot as plt
# --- SHAP for LightGBM ---
# Create SHAP explainer for the trained model
explainer = shap.TreeExplainer(final_model)
# Compute SHAP values
shap_values = explainer.shap_values(X_res)
# --- Summary (Beeswarm) Plot ---
plt.figure()
shap.summary_plot(shap_values, X_res, plot_type="dot", show=False)
plt.title("SHAP Feature Importance (Bar Plot)")
plt.tight_layout()
plt.show()
# --- Mean Absolute SHAP Values (Bar Plot) ---
plt.figure()
shap.summary_plot(shap_values, X_res, plot_type="bar", show=False)
plt.title("SHAP Beeswarm Plot (Feature Impact")
plt.tight_layout()
plt.show()