emailspamclassifier / src /evaluator.py
vettri06's picture
Add inference source modules
e40ee72 verified
Raw History Blame Contribute Delete
5.46 kB
"""
Model evaluation module:
Computes comprehensive multi-class metrics, security-oriented performance indices,
confusion matrices, and visualization plots.
"""
import os
import matplotlib
matplotlib.use("Agg") # Headless backend
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
from typing import Dict, Any, List
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
classification_report, confusion_matrix, roc_auc_score
)
from src.data_loader import CLASS_NAMES, ID_TO_CLASS
def evaluate_predictions(
y_true: np.ndarray,
y_pred: np.ndarray,
y_proba: np.ndarray = None,
dataset_name: str = "validation"
) -> Dict[str, Any]:
"""
Computes rigorous evaluation metrics with a focus on security metrics.
For email threats, false negatives on 'Suspicious' can lead to breaches.
"""
acc = accuracy_score(y_true, y_pred)
macro_p = precision_score(y_true, y_pred, average="macro", zero_division=0)
macro_r = recall_score(y_true, y_pred, average="macro", zero_division=0)
macro_f1 = f1_score(y_true, y_pred, average="macro", zero_division=0)
weighted_p = precision_score(y_true, y_pred, average="weighted", zero_division=0)
weighted_r = recall_score(y_true, y_pred, average="weighted", zero_division=0)
weighted_f1 = f1_score(y_true, y_pred, average="weighted", zero_division=0)
# Per-class metrics
class_p = precision_score(y_true, y_pred, average=None, zero_division=0)
class_r = recall_score(y_true, y_pred, average=None, zero_division=0)
class_f1 = f1_score(y_true, y_pred, average=None, zero_division=0)
cm = confusion_matrix(y_true, y_pred)
report_str = classification_report(
y_true, y_pred, target_names=CLASS_NAMES, digits=4, zero_division=0
)
metrics = {
f"{dataset_name}_accuracy": float(acc),
f"{dataset_name}_macro_precision": float(macro_p),
f"{dataset_name}_macro_recall": float(macro_r),
f"{dataset_name}_macro_f1": float(macro_f1),
f"{dataset_name}_weighted_f1": float(weighted_f1),
}
# Add per-class specific metrics
for idx, name in enumerate(CLASS_NAMES):
clean_name = name.lower()
metrics[f"{dataset_name}_{clean_name}_precision"] = float(class_p[idx])
metrics[f"{dataset_name}_{clean_name}_recall"] = float(class_r[idx])
metrics[f"{dataset_name}_{clean_name}_f1"] = float(class_f1[idx])
# Security false negative rate for Suspicious class (class index 2)
suspicious_idx = 2
suspicious_fnr = 1.0 - float(class_r[suspicious_idx])
metrics[f"{dataset_name}_suspicious_false_negative_rate"] = float(suspicious_fnr)
# Multi-class ROC-AUC if probabilities provided
if y_proba is not None:
try:
auc = roc_auc_score(y_true, y_proba, multi_class="ovr", average="macro")
metrics[f"{dataset_name}_roc_auc_ovr"] = float(auc)
except Exception:
metrics[f"{dataset_name}_roc_auc_ovr"] = 0.0
return {
"metrics": metrics,
"confusion_matrix": cm,
"classification_report": report_str
}
def plot_confusion_matrix(
cm: np.ndarray,
model_name: str,
output_path: str
) -> str:
"""
Renders and saves a high-resolution confusion matrix heatmap.
"""
os.makedirs(os.path.dirname(output_path), exist_ok=True)
plt.figure(figsize=(7, 6))
# Normalize for percentage annotations
cm_norm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
annotations = np.empty_like(cm, dtype=object)
for i in range(cm.shape[0]):
for j in range(cm.shape[1]):
annotations[i, j] = f"{cm[i, j]}\n({cm_norm[i, j]*100:.1f}%)"
sns.heatmap(
cm,
annot=annotations,
fmt="",
cmap="Blues",
xticklabels=CLASS_NAMES,
yticklabels=CLASS_NAMES,
cbar=True,
annot_kws={"size": 11, "weight": "bold"}
)
plt.title(f"Confusion Matrix: {model_name}", fontsize=13, pad=12, weight="bold")
plt.xlabel("Predicted Class", fontsize=11, weight="bold")
plt.ylabel("True Class", fontsize=11, weight="bold")
plt.tight_layout()
plt.savefig(output_path, dpi=200)
plt.close()
return output_path
def plot_metric_comparisons(
comparison_df: pd.DataFrame,
output_path: str
) -> str:
"""
Plots a multi-bar chart comparing model performances across key metrics.
"""
os.makedirs(os.path.dirname(output_path), exist_ok=True)
plt.figure(figsize=(10, 6))
# Melt dataframe for seaborn barplot
metrics_to_plot = ["Macro F1", "Suspicious Recall", "Accuracy", "Weighted F1"]
melted = comparison_df.melt(
id_vars=["Model"],
value_vars=[m for m in metrics_to_plot if m in comparison_df.columns],
var_name="Metric",
value_name="Score"
)
sns.barplot(
data=melted,
x="Metric",
y="Score",
hue="Model",
palette="crest"
)
plt.title("Model Performance Benchmark Comparison", fontsize=13, weight="bold", pad=12)
plt.ylim(0.0, 1.05)
plt.ylabel("Score (0.0 - 1.0)", fontsize=11, weight="bold")
plt.xlabel("Evaluation Metric", fontsize=11, weight="bold")
plt.legend(loc="lower right")
plt.grid(axis="y", linestyle="--", alpha=0.5)
plt.tight_layout()
plt.savefig(output_path, dpi=200)
plt.close()
return output_path