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(
No description has been provided for this image

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()
No description has been provided for this image

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(
No description has been provided for this image

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()
No description has been provided for this image
No description has been provided for this image