Spaces:
Runtime error
Runtime error
Upload 44 files
Browse files- Version-3/cli.py +251 -0
- Version-3/models/SVM_model.pkl +3 -0
- Version-3/models/vectorizer.pkl +3 -0
- Version-3/outputs/2025-12-25_10-01-23/models/SVM_model.pkl +3 -0
- Version-3/outputs/2025-12-25_10-01-23/models/vectorizer.pkl +3 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/DecisionTree_detailed_metrics.csv +6 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/KNN_detailed_metrics.csv +6 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/LogisticRegression_detailed_metrics.csv +6 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/RandomForest_detailed_metrics.csv +6 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/SVM_detailed_metrics.csv +6 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/best_model_info.csv +12 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/best_parameters.csv +29 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/cross_validation_summary.csv +6 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/model_comparison_summary.csv +6 -0
- Version-3/outputs/2025-12-25_10-01-23/observations/model_metadata.csv +2 -0
- Version-3/outputs/2025-12-25_14-02-05/models/SVM_model.pkl +3 -0
- Version-3/outputs/2025-12-25_14-02-05/models/vectorizer.pkl +3 -0
- Version-3/outputs/2025-12-25_14-02-05/observations/best_model_info.csv +12 -0
- Version-3/outputs/2025-12-25_14-02-05/observations/best_parameters.csv +29 -0
- Version-3/outputs/2025-12-25_14-02-05/observations/cross_validation_summary.csv +6 -0
- Version-3/outputs/2025-12-25_14-02-05/observations/model_comparison_summary.csv +6 -0
- Version-3/outputs/2025-12-25_14-02-05/observations/model_metadata.csv +2 -0
- Version-3/requirements.txt +8 -0
- Version-3/src/__init__.py +0 -0
- Version-3/src/__pycache__/__init__.cpython-313.pyc +0 -0
- Version-3/src/components/data_ingestion.py +21 -0
- Version-3/src/components/data_transformation.py +65 -0
- Version-3/src/components/model_training.py +253 -0
- Version-3/src/config/__pycache__/config.cpython-313.pyc +0 -0
- Version-3/src/config/config.py +41 -0
- Version-3/src/pipeline/__init__.py +0 -0
- Version-3/src/pipeline/__pycache__/__init__.cpython-313.pyc +0 -0
- Version-3/src/pipeline/__pycache__/prediction_pipeline.cpython-313.pyc +0 -0
- Version-3/src/pipeline/prediction_pipeline.py +135 -0
- Version-3/src/pipeline/training_pipeline.py +52 -0
- Version-3/src/utils/__init__.py +0 -0
- Version-3/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- Version-3/src/utils/__pycache__/email_utils.cpython-313.pyc +0 -0
- Version-3/src/utils/__pycache__/logger.cpython-313.pyc +0 -0
- Version-3/src/utils/__pycache__/state.cpython-313.pyc +0 -0
- Version-3/src/utils/email_utils.py +54 -0
- Version-3/src/utils/logger.py +34 -0
- Version-3/src/utils/state.py +24 -0
- Version-3/src/utils/utils.py +0 -0
Version-3/cli.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pickle
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import mailbox
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
from html import unescape
|
| 9 |
+
from bs4 import BeautifulSoup
|
| 10 |
+
|
| 11 |
+
MODEL_PATH = "models/SVM_model.pkl"
|
| 12 |
+
FEATURE_PATH = "models/vectorizer.pkl"
|
| 13 |
+
|
| 14 |
+
# ── text helpers ─────────────────────────────────────────────────────────────
|
| 15 |
+
def clean_text(text):
|
| 16 |
+
if not isinstance(text, str):
|
| 17 |
+
return text
|
| 18 |
+
text = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\u200B-\u200F\uFEFF]', '', text)
|
| 19 |
+
text = text.encode("utf-16", "surrogatepass").decode("utf-16", "ignore")
|
| 20 |
+
return text[:32767]
|
| 21 |
+
|
| 22 |
+
def extract_body(msg):
|
| 23 |
+
texts = []
|
| 24 |
+
if msg.is_multipart():
|
| 25 |
+
for part in msg.walk():
|
| 26 |
+
if part.get_content_type() in ("text/plain", "text/html"):
|
| 27 |
+
payload = part.get_payload(decode=True)
|
| 28 |
+
if payload:
|
| 29 |
+
text = unescape(payload.decode(errors="ignore"))
|
| 30 |
+
text = BeautifulSoup(text, "html.parser").get_text(" ")
|
| 31 |
+
texts.append(text)
|
| 32 |
+
else:
|
| 33 |
+
payload = msg.get_payload(decode=True)
|
| 34 |
+
if payload:
|
| 35 |
+
text = unescape(payload.decode(errors="ignore"))
|
| 36 |
+
text = BeautifulSoup(text, "html.parser").get_text(" ")
|
| 37 |
+
texts.append(text)
|
| 38 |
+
combined = " ".join(texts)
|
| 39 |
+
combined = re.sub(r'[\r\n\t]+', ' ', combined)
|
| 40 |
+
combined = re.sub(r'\s+', ' ', combined)
|
| 41 |
+
return combined.strip()
|
| 42 |
+
|
| 43 |
+
# ── model ─────────────────────────────────────────────────────────────────────
|
| 44 |
+
def load_models():
|
| 45 |
+
for path in (MODEL_PATH, FEATURE_PATH):
|
| 46 |
+
if not os.path.exists(path):
|
| 47 |
+
print(f"[ERROR] File not found: {path}")
|
| 48 |
+
sys.exit(1)
|
| 49 |
+
vectorizer = pickle.load(open(FEATURE_PATH, "rb"))
|
| 50 |
+
model = pickle.load(open(MODEL_PATH, "rb"))
|
| 51 |
+
return vectorizer, model
|
| 52 |
+
|
| 53 |
+
def sigmoid(x):
|
| 54 |
+
import math
|
| 55 |
+
return 1 / (1 + math.exp(-x))
|
| 56 |
+
|
| 57 |
+
def predict(text, vectorizer, model):
|
| 58 |
+
"""
|
| 59 |
+
Returns:
|
| 60 |
+
label : "Spam" or "Ham"
|
| 61 |
+
confidence : probability % if model supports it, else None
|
| 62 |
+
spam_score : 0-100 spam intensity score (always available)
|
| 63 |
+
"""
|
| 64 |
+
cleaned = clean_text(text)
|
| 65 |
+
features = vectorizer.transform([cleaned])
|
| 66 |
+
pred = model.predict(features)[0]
|
| 67 |
+
label = "Spam" if str(pred) == "0" else "Ham"
|
| 68 |
+
|
| 69 |
+
# confidence via predict_proba (not all SVMs support this)
|
| 70 |
+
confidence = None
|
| 71 |
+
try:
|
| 72 |
+
proba = model.predict_proba(features)
|
| 73 |
+
# index 0 = spam class (label 0), index 1 = ham class (label 1)
|
| 74 |
+
confidence = round(float(proba[0][0]) * 100, 1)
|
| 75 |
+
except Exception:
|
| 76 |
+
pass
|
| 77 |
+
|
| 78 |
+
# spam score via decision function (always works for SVM)
|
| 79 |
+
# decision_function > 0 means ham, < 0 means spam for binary SVC
|
| 80 |
+
# we flip and sigmoid-scale so higher = more spammy
|
| 81 |
+
spam_score = None
|
| 82 |
+
try:
|
| 83 |
+
df_val = float(model.decision_function(features)[0])
|
| 84 |
+
# flip: spam has negative decision value in sklearn SVC (class 0)
|
| 85 |
+
spam_score = round(sigmoid(-df_val) * 100, 1)
|
| 86 |
+
except Exception:
|
| 87 |
+
pass
|
| 88 |
+
|
| 89 |
+
# fall back score to confidence if decision_function unavailable
|
| 90 |
+
if spam_score is None and confidence is not None:
|
| 91 |
+
spam_score = confidence
|
| 92 |
+
|
| 93 |
+
return label, confidence, spam_score
|
| 94 |
+
|
| 95 |
+
# ── display helpers ───────────────────────────────────────────────────────────
|
| 96 |
+
def score_bar(score, width=20):
|
| 97 |
+
"""Visual bar: [████████░░░░░░░░░░░░] 42.3"""
|
| 98 |
+
if score is None:
|
| 99 |
+
return "N/A"
|
| 100 |
+
filled = round(score / 100 * width)
|
| 101 |
+
bar = "█" * filled + "░" * (width - filled)
|
| 102 |
+
return f"[{bar}] {score:.1f}%"
|
| 103 |
+
|
| 104 |
+
def risk_level(score):
|
| 105 |
+
if score is None:
|
| 106 |
+
return "Unknown"
|
| 107 |
+
if score >= 80:
|
| 108 |
+
return "HIGH RISK"
|
| 109 |
+
if score >= 50:
|
| 110 |
+
return "MEDIUM RISK"
|
| 111 |
+
return "LOW RISK"
|
| 112 |
+
|
| 113 |
+
def print_result(label, confidence, spam_score):
|
| 114 |
+
verdict = "*** SPAM ***" if label == "Spam" else "Ham (Safe) "
|
| 115 |
+
print(f"\n Verdict : {verdict}")
|
| 116 |
+
print(f" Spam Score : {score_bar(spam_score)}")
|
| 117 |
+
if confidence is not None:
|
| 118 |
+
print(f" Confidence : {confidence:.1f}%")
|
| 119 |
+
print(f" Risk Level : {risk_level(spam_score)}")
|
| 120 |
+
print()
|
| 121 |
+
|
| 122 |
+
# ── modes ─────────────────────────────────────────────────────────────────────
|
| 123 |
+
def mode_interactive(vectorizer, model):
|
| 124 |
+
print("\n=== Spam Email Detector ===")
|
| 125 |
+
print("Paste your email text. Press Enter twice to classify.")
|
| 126 |
+
print("Type 'quit' to exit.\n")
|
| 127 |
+
while True:
|
| 128 |
+
print("─" * 44)
|
| 129 |
+
lines = []
|
| 130 |
+
blank_count = 0
|
| 131 |
+
while True:
|
| 132 |
+
try:
|
| 133 |
+
line = input()
|
| 134 |
+
except EOFError:
|
| 135 |
+
break
|
| 136 |
+
if line.strip().lower() in ("quit", "exit"):
|
| 137 |
+
print("Goodbye.")
|
| 138 |
+
sys.exit(0)
|
| 139 |
+
if line.strip() == "":
|
| 140 |
+
blank_count += 1
|
| 141 |
+
if blank_count >= 2:
|
| 142 |
+
break
|
| 143 |
+
else:
|
| 144 |
+
blank_count = 0
|
| 145 |
+
lines.append(line)
|
| 146 |
+
|
| 147 |
+
text = "\n".join(lines).strip()
|
| 148 |
+
if not text:
|
| 149 |
+
print("[!] No text entered. Try again.\n")
|
| 150 |
+
continue
|
| 151 |
+
|
| 152 |
+
label, confidence, spam_score = predict(text, vectorizer, model)
|
| 153 |
+
print_result(label, confidence, spam_score)
|
| 154 |
+
|
| 155 |
+
def mode_single(text, vectorizer, model):
|
| 156 |
+
label, confidence, spam_score = predict(text, vectorizer, model)
|
| 157 |
+
print_result(label, confidence, spam_score)
|
| 158 |
+
|
| 159 |
+
def mode_mbox(mbox_path, vectorizer, model, output_csv):
|
| 160 |
+
if not os.path.exists(mbox_path):
|
| 161 |
+
print(f"[ERROR] File not found: {mbox_path}")
|
| 162 |
+
sys.exit(1)
|
| 163 |
+
print(f"Loading: {mbox_path}")
|
| 164 |
+
mbox = mailbox.mbox(mbox_path)
|
| 165 |
+
messages = list(mbox)
|
| 166 |
+
print(f"Found {len(messages)} emails. Classifying...\n")
|
| 167 |
+
|
| 168 |
+
results = []
|
| 169 |
+
spam_count = 0
|
| 170 |
+
scores = []
|
| 171 |
+
|
| 172 |
+
for i, msg in enumerate(messages, 1):
|
| 173 |
+
subject = clean_text(msg.get("Subject", "(no subject)"))
|
| 174 |
+
body = extract_body(msg)
|
| 175 |
+
label, confidence, spam_score = predict(body, vectorizer, model)
|
| 176 |
+
|
| 177 |
+
if label == "Spam":
|
| 178 |
+
spam_count += 1
|
| 179 |
+
if spam_score is not None:
|
| 180 |
+
scores.append(spam_score)
|
| 181 |
+
|
| 182 |
+
conf_str = f"{confidence:.1f}%" if confidence is not None else "N/A"
|
| 183 |
+
score_str = f"{spam_score:.1f}%" if spam_score is not None else "N/A"
|
| 184 |
+
risk = risk_level(spam_score)
|
| 185 |
+
marker = "SPAM" if label == "Spam" else "Ham "
|
| 186 |
+
|
| 187 |
+
results.append({
|
| 188 |
+
"Index": i,
|
| 189 |
+
"Subject": subject,
|
| 190 |
+
"Prediction": label,
|
| 191 |
+
"SpamScore": score_str,
|
| 192 |
+
"Confidence": conf_str,
|
| 193 |
+
"RiskLevel": risk,
|
| 194 |
+
})
|
| 195 |
+
print(f" [{i:>4}] {marker} Score:{score_str:>6} {risk:<11} {subject[:50]}")
|
| 196 |
+
|
| 197 |
+
ham_count = len(results) - spam_count
|
| 198 |
+
avg_score = round(sum(scores) / len(scores), 1) if scores else 0
|
| 199 |
+
|
| 200 |
+
print(f"\n{'─'*44}")
|
| 201 |
+
print(f" Total emails : {len(results)}")
|
| 202 |
+
print(f" Spam : {spam_count}")
|
| 203 |
+
print(f" Ham : {ham_count}")
|
| 204 |
+
print(f" Avg Spam Score : {avg_score}%")
|
| 205 |
+
|
| 206 |
+
if output_csv:
|
| 207 |
+
with open(output_csv, "w", newline="", encoding="utf-8") as f:
|
| 208 |
+
writer = csv.DictWriter(f, fieldnames=["Index","Subject","Prediction","SpamScore","Confidence","RiskLevel"])
|
| 209 |
+
writer.writeheader()
|
| 210 |
+
writer.writerows(results)
|
| 211 |
+
print(f"\n Saved to: {output_csv}")
|
| 212 |
+
|
| 213 |
+
# ── entry point ───────────────────────────────────────────────────────────────
|
| 214 |
+
def main():
|
| 215 |
+
parser = argparse.ArgumentParser(
|
| 216 |
+
description="Spam Email Detector — CMD",
|
| 217 |
+
formatter_class=argparse.RawTextHelpFormatter,
|
| 218 |
+
epilog="""
|
| 219 |
+
Examples:
|
| 220 |
+
python cli.py # interactive
|
| 221 |
+
python cli.py --text "You won a prize! Click now." # inline text
|
| 222 |
+
python cli.py --file email.txt # from file
|
| 223 |
+
python cli.py --mbox inbox.mbox --output results.csv # batch mbox
|
| 224 |
+
"""
|
| 225 |
+
)
|
| 226 |
+
parser.add_argument("--text", help="Email text to classify (inline)")
|
| 227 |
+
parser.add_argument("--file", help="Path to a .txt file with email content")
|
| 228 |
+
parser.add_argument("--mbox", help="Path to an .mbox file for batch classification")
|
| 229 |
+
parser.add_argument("--output", help="CSV file to save mbox results (optional)")
|
| 230 |
+
args = parser.parse_args()
|
| 231 |
+
|
| 232 |
+
print("Loading models...", end=" ", flush=True)
|
| 233 |
+
vectorizer, model = load_models()
|
| 234 |
+
print("OK\n")
|
| 235 |
+
|
| 236 |
+
if args.mbox:
|
| 237 |
+
mode_mbox(args.mbox, vectorizer, model, args.output)
|
| 238 |
+
elif args.text:
|
| 239 |
+
mode_single(args.text, vectorizer, model)
|
| 240 |
+
elif args.file:
|
| 241 |
+
if not os.path.exists(args.file):
|
| 242 |
+
print(f"[ERROR] File not found: {args.file}")
|
| 243 |
+
sys.exit(1)
|
| 244 |
+
with open(args.file, "r", encoding="utf-8", errors="ignore") as f:
|
| 245 |
+
text = f.read()
|
| 246 |
+
mode_single(text, vectorizer, model)
|
| 247 |
+
else:
|
| 248 |
+
mode_interactive(vectorizer, model)
|
| 249 |
+
|
| 250 |
+
if __name__ == "__main__":
|
| 251 |
+
main()
|
Version-3/models/SVM_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d4b7ec310866553c9aeeacf3f69abb592b584309cb1b9392281335cd73e8cf7
|
| 3 |
+
size 131573
|
Version-3/models/vectorizer.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7caca664beb75eeba23cc9f30dfcda1c0ada3a4f7bfd281452ef8aea2115310f
|
| 3 |
+
size 138244
|
Version-3/outputs/2025-12-25_10-01-23/models/SVM_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d4b7ec310866553c9aeeacf3f69abb592b584309cb1b9392281335cd73e8cf7
|
| 3 |
+
size 131573
|
Version-3/outputs/2025-12-25_10-01-23/models/vectorizer.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d75c40fde3736879643d139e83e814309032478c54d5e9f07acb8051996ccef9
|
| 3 |
+
size 138244
|
Version-3/outputs/2025-12-25_10-01-23/observations/DecisionTree_detailed_metrics.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metric,Value
|
| 2 |
+
Accuracy,0.9599282296650717
|
| 3 |
+
Precision,0.9588033625788168
|
| 4 |
+
Recall,0.9599282296650717
|
| 5 |
+
F1-Score,0.9588283410865664
|
| 6 |
+
CV Score,0.9834796073961313
|
Version-3/outputs/2025-12-25_10-01-23/observations/KNN_detailed_metrics.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metric,Value
|
| 2 |
+
Accuracy,0.93122009569378
|
| 3 |
+
Precision,0.9362806772646151
|
| 4 |
+
Recall,0.93122009569378
|
| 5 |
+
F1-Score,0.9206571512502397
|
| 6 |
+
CV Score,0.9612898978068009
|
Version-3/outputs/2025-12-25_10-01-23/observations/LogisticRegression_detailed_metrics.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metric,Value
|
| 2 |
+
Accuracy,0.9766746411483254
|
| 3 |
+
Precision,0.9767291368060467
|
| 4 |
+
Recall,0.9766746411483254
|
| 5 |
+
F1-Score,0.9759311514144211
|
| 6 |
+
CV Score,0.9876984264199944
|
Version-3/outputs/2025-12-25_10-01-23/observations/RandomForest_detailed_metrics.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metric,Value
|
| 2 |
+
Accuracy,0.9706937799043063
|
| 3 |
+
Precision,0.9714374960389777
|
| 4 |
+
Recall,0.9706937799043063
|
| 5 |
+
F1-Score,0.9692103447020837
|
| 6 |
+
CV Score,0.9878592900446806
|
Version-3/outputs/2025-12-25_10-01-23/observations/SVM_detailed_metrics.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metric,Value
|
| 2 |
+
Accuracy,0.979066985645933
|
| 3 |
+
Precision,0.9789275513603889
|
| 4 |
+
Recall,0.979066985645933
|
| 5 |
+
F1-Score,0.9785830328138276
|
| 6 |
+
CV Score,0.9891548320127734
|
Version-3/outputs/2025-12-25_10-01-23/observations/best_model_info.csv
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Attribute,Value
|
| 2 |
+
Best Model Name,SVM
|
| 3 |
+
Accuracy,0.979066985645933
|
| 4 |
+
Precision,0.9789275513603889
|
| 5 |
+
Recall,0.979066985645933
|
| 6 |
+
F1-Score,0.9785830328138276
|
| 7 |
+
CV Score,0.9891548320127734
|
| 8 |
+
Best Parameters,"{
|
| 9 |
+
""C"": 10,
|
| 10 |
+
""gamma"": ""scale"",
|
| 11 |
+
""kernel"": ""linear""
|
| 12 |
+
}"
|
Version-3/outputs/2025-12-25_10-01-23/observations/best_parameters.csv
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model,Best_Parameters,CV_Score
|
| 2 |
+
LogisticRegression,"{
|
| 3 |
+
""C"": 100,
|
| 4 |
+
""max_iter"": 100,
|
| 5 |
+
""solver"": ""liblinear""
|
| 6 |
+
}",0.9876984264199944
|
| 7 |
+
DecisionTree,"{
|
| 8 |
+
""criterion"": ""gini"",
|
| 9 |
+
""max_depth"": null,
|
| 10 |
+
""min_samples_leaf"": 1,
|
| 11 |
+
""min_samples_split"": 2
|
| 12 |
+
}",0.9834796073961313
|
| 13 |
+
SVM,"{
|
| 14 |
+
""C"": 10,
|
| 15 |
+
""gamma"": ""scale"",
|
| 16 |
+
""kernel"": ""linear""
|
| 17 |
+
}",0.9891548320127734
|
| 18 |
+
KNN,"{
|
| 19 |
+
""metric"": ""manhattan"",
|
| 20 |
+
""n_neighbors"": 3,
|
| 21 |
+
""weights"": ""distance""
|
| 22 |
+
}",0.9612898978068009
|
| 23 |
+
RandomForest,"{
|
| 24 |
+
""max_depth"": null,
|
| 25 |
+
""max_features"": ""sqrt"",
|
| 26 |
+
""min_samples_leaf"": 1,
|
| 27 |
+
""min_samples_split"": 5,
|
| 28 |
+
""n_estimators"": 50
|
| 29 |
+
}",0.9878592900446806
|
Version-3/outputs/2025-12-25_10-01-23/observations/cross_validation_summary.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model,Best_CV_Score,Best_Parameters
|
| 2 |
+
LogisticRegression,0.9876984264199944,"{""C"": 100, ""max_iter"": 100, ""solver"": ""liblinear""}"
|
| 3 |
+
DecisionTree,0.9834796073961313,"{""criterion"": ""gini"", ""max_depth"": null, ""min_samples_leaf"": 1, ""min_samples_split"": 2}"
|
| 4 |
+
SVM,0.9891548320127734,"{""C"": 10, ""gamma"": ""scale"", ""kernel"": ""linear""}"
|
| 5 |
+
KNN,0.9612898978068009,"{""metric"": ""manhattan"", ""n_neighbors"": 3, ""weights"": ""distance""}"
|
| 6 |
+
RandomForest,0.9878592900446806,"{""max_depth"": null, ""max_features"": ""sqrt"", ""min_samples_leaf"": 1, ""min_samples_split"": 5, ""n_estimators"": 50}"
|
Version-3/outputs/2025-12-25_10-01-23/observations/model_comparison_summary.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model,Accuracy,Precision,Recall,F1_Score,CV_Score,Is_Best_Model
|
| 2 |
+
SVM,0.979066985645933,0.9789275513603889,0.979066985645933,0.9785830328138276,0.9891548320127734,1
|
| 3 |
+
LogisticRegression,0.9766746411483254,0.9767291368060467,0.9766746411483254,0.9759311514144211,0.9876984264199944,0
|
| 4 |
+
RandomForest,0.9706937799043063,0.9714374960389777,0.9706937799043063,0.9692103447020837,0.9878592900446806,0
|
| 5 |
+
DecisionTree,0.9599282296650717,0.9588033625788168,0.9599282296650717,0.9588283410865664,0.9834796073961313,0
|
| 6 |
+
KNN,0.93122009569378,0.9362806772646151,0.93122009569378,0.9206571512502397,0.9612898978068009,0
|
Version-3/outputs/2025-12-25_10-01-23/observations/model_metadata.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
timestamp,best_model_name,best_model_params,best_model_metrics,all_models,tfidf_features,train_samples,test_samples
|
| 2 |
+
2025-12-25_10-01-23,SVM,"{'C': 10, 'gamma': 'scale', 'kernel': 'linear'}","{'accuracy': 0.979066985645933, 'precision': 0.9789275513603889, 'recall': 0.979066985645933, 'f1_score': 0.9785830328138276, 'best_params': {'C': 10, 'gamma': 'scale', 'kernel': 'linear'}, 'best_cv_score': np.float64(0.9891548320127734)}","LogisticRegression, DecisionTree, SVM, KNN, RandomForest",6847,3900,1672
|
Version-3/outputs/2025-12-25_14-02-05/models/SVM_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d4b7ec310866553c9aeeacf3f69abb592b584309cb1b9392281335cd73e8cf7
|
| 3 |
+
size 131573
|
Version-3/outputs/2025-12-25_14-02-05/models/vectorizer.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7caca664beb75eeba23cc9f30dfcda1c0ada3a4f7bfd281452ef8aea2115310f
|
| 3 |
+
size 138244
|
Version-3/outputs/2025-12-25_14-02-05/observations/best_model_info.csv
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Attribute,Value
|
| 2 |
+
Best Model Name,SVM
|
| 3 |
+
Accuracy,0.979066985645933
|
| 4 |
+
Precision,0.9789275513603889
|
| 5 |
+
Recall,0.979066985645933
|
| 6 |
+
F1-Score,0.9785830328138276
|
| 7 |
+
CV Score,0.9891548320127734
|
| 8 |
+
Best Parameters,"{
|
| 9 |
+
""C"": 10,
|
| 10 |
+
""gamma"": ""scale"",
|
| 11 |
+
""kernel"": ""linear""
|
| 12 |
+
}"
|
Version-3/outputs/2025-12-25_14-02-05/observations/best_parameters.csv
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model,Best_Parameters,CV_Score
|
| 2 |
+
LogisticRegression,"{
|
| 3 |
+
""C"": 100,
|
| 4 |
+
""max_iter"": 100,
|
| 5 |
+
""solver"": ""liblinear""
|
| 6 |
+
}",0.9876984264199944
|
| 7 |
+
DecisionTree,"{
|
| 8 |
+
""criterion"": ""gini"",
|
| 9 |
+
""max_depth"": null,
|
| 10 |
+
""min_samples_leaf"": 1,
|
| 11 |
+
""min_samples_split"": 2
|
| 12 |
+
}",0.9834796073961313
|
| 13 |
+
SVM,"{
|
| 14 |
+
""C"": 10,
|
| 15 |
+
""gamma"": ""scale"",
|
| 16 |
+
""kernel"": ""linear""
|
| 17 |
+
}",0.9891548320127734
|
| 18 |
+
KNN,"{
|
| 19 |
+
""metric"": ""manhattan"",
|
| 20 |
+
""n_neighbors"": 3,
|
| 21 |
+
""weights"": ""distance""
|
| 22 |
+
}",0.9612898978068009
|
| 23 |
+
RandomForest,"{
|
| 24 |
+
""max_depth"": null,
|
| 25 |
+
""max_features"": ""sqrt"",
|
| 26 |
+
""min_samples_leaf"": 1,
|
| 27 |
+
""min_samples_split"": 5,
|
| 28 |
+
""n_estimators"": 50
|
| 29 |
+
}",0.9878592900446806
|
Version-3/outputs/2025-12-25_14-02-05/observations/cross_validation_summary.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model,Best_CV_Score,Best_Parameters
|
| 2 |
+
LogisticRegression,0.9876984264199944,"{""C"": 100, ""max_iter"": 100, ""solver"": ""liblinear""}"
|
| 3 |
+
DecisionTree,0.9834796073961313,"{""criterion"": ""gini"", ""max_depth"": null, ""min_samples_leaf"": 1, ""min_samples_split"": 2}"
|
| 4 |
+
SVM,0.9891548320127734,"{""C"": 10, ""gamma"": ""scale"", ""kernel"": ""linear""}"
|
| 5 |
+
KNN,0.9612898978068009,"{""metric"": ""manhattan"", ""n_neighbors"": 3, ""weights"": ""distance""}"
|
| 6 |
+
RandomForest,0.9878592900446806,"{""max_depth"": null, ""max_features"": ""sqrt"", ""min_samples_leaf"": 1, ""min_samples_split"": 5, ""n_estimators"": 50}"
|
Version-3/outputs/2025-12-25_14-02-05/observations/model_comparison_summary.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Model,Accuracy,Precision,Recall,F1_Score,CV_Score,Is_Best_Model
|
| 2 |
+
SVM,0.979066985645933,0.9789275513603889,0.979066985645933,0.9785830328138276,0.9891548320127734,1
|
| 3 |
+
LogisticRegression,0.9766746411483254,0.9767291368060467,0.9766746411483254,0.9759311514144211,0.9876984264199944,0
|
| 4 |
+
RandomForest,0.9706937799043063,0.9714374960389777,0.9706937799043063,0.9692103447020837,0.9878592900446806,0
|
| 5 |
+
DecisionTree,0.9599282296650717,0.9588033625788168,0.9599282296650717,0.9588283410865664,0.9834796073961313,0
|
| 6 |
+
KNN,0.93122009569378,0.9362806772646151,0.93122009569378,0.9206571512502397,0.9612898978068009,0
|
Version-3/outputs/2025-12-25_14-02-05/observations/model_metadata.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
timestamp,best_model_name,best_model_params,best_model_metrics,all_models,tfidf_features,train_samples,test_samples
|
| 2 |
+
2025-12-25_14-02-05,SVM,"{'C': 10, 'gamma': 'scale', 'kernel': 'linear'}","{'accuracy': 0.979066985645933, 'precision': 0.9789275513603889, 'recall': 0.979066985645933, 'f1_score': 0.9785830328138276, 'best_params': {'C': 10, 'gamma': 'scale', 'kernel': 'linear'}, 'best_cv_score': np.float64(0.9891548320127734)}","LogisticRegression, DecisionTree, SVM, KNN, RandomForest",6847,3900,1672
|
Version-3/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn[standard]==0.34.0
|
| 3 |
+
python-multipart==0.0.20
|
| 4 |
+
pandas==2.2.3
|
| 5 |
+
beautifulsoup4==4.12.3
|
| 6 |
+
scikit-learn==1.6.1
|
| 7 |
+
pydantic==2.10.6
|
| 8 |
+
streamlit>=1.32.0
|
Version-3/src/__init__.py
ADDED
|
File without changes
|
Version-3/src/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
Version-3/src/components/data_ingestion.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from src.utils.logger import get_logger
|
| 3 |
+
from src.config.config import Config
|
| 4 |
+
from src.utils.state import TrainingState
|
| 5 |
+
|
| 6 |
+
logger = get_logger(__name__)
|
| 7 |
+
|
| 8 |
+
class DataIngestion:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.config = Config()
|
| 11 |
+
|
| 12 |
+
def load_data(self, state: TrainingState) -> TrainingState:
|
| 13 |
+
try:
|
| 14 |
+
logger.info("Loading data")
|
| 15 |
+
state.training_data = pd.read_csv(self.config.training_data_path)
|
| 16 |
+
logger.info("Data loaded successfully")
|
| 17 |
+
return state
|
| 18 |
+
except Exception as e:
|
| 19 |
+
logger.error(f"Failed to load data: {str(e)}")
|
| 20 |
+
raise e
|
| 21 |
+
|
Version-3/src/components/data_transformation.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.utils.logger import get_logger
|
| 2 |
+
from src.config.config import Config
|
| 3 |
+
from src.utils.state import TrainingState
|
| 4 |
+
from sklearn.model_selection import train_test_split
|
| 5 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 6 |
+
|
| 7 |
+
logger = get_logger(__name__)
|
| 8 |
+
|
| 9 |
+
class DataTransformation:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.config = Config()
|
| 12 |
+
|
| 13 |
+
def transform_data(self, state: TrainingState) -> TrainingState:
|
| 14 |
+
logger.info("Data transformation started")
|
| 15 |
+
try:
|
| 16 |
+
data = state.training_data.copy()
|
| 17 |
+
|
| 18 |
+
# Encode labels: spam -> 0, ham -> 1
|
| 19 |
+
data.loc[data['Category'] == 'spam', 'Category'] = 0
|
| 20 |
+
data.loc[data['Category'] == 'ham', 'Category'] = 1
|
| 21 |
+
|
| 22 |
+
# Ensure Category column is integer type
|
| 23 |
+
data['Category'] = data['Category'].astype(int)
|
| 24 |
+
|
| 25 |
+
logger.info(f"Label encoding completed. Data shape: {data.shape}")
|
| 26 |
+
logger.info(f"Unique labels: {data['Category'].unique()}")
|
| 27 |
+
logger.info(f"Label dtype: {data['Category'].dtype}")
|
| 28 |
+
|
| 29 |
+
# Split features and target
|
| 30 |
+
X = data['Message']
|
| 31 |
+
y = data['Category']
|
| 32 |
+
|
| 33 |
+
# Convert y to numpy array of integers to ensure proper type
|
| 34 |
+
import numpy as np
|
| 35 |
+
y = np.array(y, dtype=int)
|
| 36 |
+
|
| 37 |
+
# Split into train and test sets (70:30 ratio)
|
| 38 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 39 |
+
X, y, test_size=0.3, random_state=42, stratify=y
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
logger.info(f"Train/test split completed. Train size: {len(X_train)}, Test size: {len(X_test)}")
|
| 43 |
+
|
| 44 |
+
# Apply TF-IDF vectorization
|
| 45 |
+
tfidf_vectorizer = TfidfVectorizer(lowercase=True, stop_words='english')
|
| 46 |
+
X_train_tfidf = tfidf_vectorizer.fit_transform(X_train)
|
| 47 |
+
X_test_tfidf = tfidf_vectorizer.transform(X_test)
|
| 48 |
+
|
| 49 |
+
logger.info(f"TF-IDF transformation completed. Feature shape: {X_train_tfidf.shape}")
|
| 50 |
+
|
| 51 |
+
# Save to state
|
| 52 |
+
state.transformed_data = data
|
| 53 |
+
state.X_train = X_train
|
| 54 |
+
state.X_test = X_test
|
| 55 |
+
state.y_train = y_train
|
| 56 |
+
state.y_test = y_test
|
| 57 |
+
state.X_train_tfidf = X_train_tfidf
|
| 58 |
+
state.X_test_tfidf = X_test_tfidf
|
| 59 |
+
state.tfidf_vectorizer = tfidf_vectorizer
|
| 60 |
+
|
| 61 |
+
logger.info("Data transformation completed")
|
| 62 |
+
return state
|
| 63 |
+
except Exception as e:
|
| 64 |
+
logger.error(f"Failed to transform data: {str(e)}")
|
| 65 |
+
raise e
|
Version-3/src/components/model_training.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import json
|
| 4 |
+
import pickle
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
|
| 10 |
+
from sklearn.svm import SVC
|
| 11 |
+
from sklearn.tree import DecisionTreeClassifier
|
| 12 |
+
from sklearn.neighbors import KNeighborsClassifier
|
| 13 |
+
from sklearn.linear_model import LogisticRegression
|
| 14 |
+
from sklearn.ensemble import RandomForestClassifier, StackingClassifier
|
| 15 |
+
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, cross_val_score
|
| 16 |
+
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, classification_report
|
| 17 |
+
|
| 18 |
+
from src.utils.logger import get_logger
|
| 19 |
+
from src.utils.state import TrainingState
|
| 20 |
+
from src.config.config import Config, ModelConfig
|
| 21 |
+
|
| 22 |
+
logger = get_logger(__name__)
|
| 23 |
+
|
| 24 |
+
class ModelTraining:
|
| 25 |
+
def __init__(self):
|
| 26 |
+
self.config = Config()
|
| 27 |
+
self.param_grids = ModelConfig.models
|
| 28 |
+
|
| 29 |
+
def save_pickle_files(self, state: TrainingState):
|
| 30 |
+
try:
|
| 31 |
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
| 32 |
+
output_dir = os.path.join(self.config.OUTPUT_BASE_DIR, timestamp)
|
| 33 |
+
models_dir = os.path.join(output_dir, "models")
|
| 34 |
+
observations_dir = os.path.join(output_dir, "observations")
|
| 35 |
+
|
| 36 |
+
os.makedirs(models_dir, exist_ok=True)
|
| 37 |
+
os.makedirs(observations_dir, exist_ok=True)
|
| 38 |
+
|
| 39 |
+
vectorizer_path = os.path.join(models_dir, "vectorizer.pkl")
|
| 40 |
+
with open(vectorizer_path, 'wb') as f:
|
| 41 |
+
pickle.dump(state.tfidf_vectorizer, f)
|
| 42 |
+
logger.info(f"Saved TF-IDF vectorizer: {vectorizer_path}")
|
| 43 |
+
|
| 44 |
+
best_model_path = os.path.join(models_dir, f"{state.best_model_name}_model.pkl")
|
| 45 |
+
with open(best_model_path, 'wb') as f:
|
| 46 |
+
pickle.dump(state.best_model, f)
|
| 47 |
+
logger.info(f"Saved best model: {state.best_model_name}_model.pkl")
|
| 48 |
+
|
| 49 |
+
metadata = {
|
| 50 |
+
'timestamp': timestamp,
|
| 51 |
+
'best_model_name': state.best_model_name,
|
| 52 |
+
'best_model_params': str(state.best_params),
|
| 53 |
+
'best_model_metrics': str(state.model_metrics[state.best_model_name]),
|
| 54 |
+
'all_models': ', '.join(list(state.trained_models.keys())),
|
| 55 |
+
'tfidf_features': state.X_train_tfidf.shape[1],
|
| 56 |
+
'train_samples': len(state.y_train),
|
| 57 |
+
'test_samples': len(state.y_test)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
metadata_path = os.path.join(observations_dir, "model_metadata.csv")
|
| 61 |
+
pd.DataFrame([metadata]).to_csv(metadata_path, index=False)
|
| 62 |
+
logger.info(f"Saved metadata: {metadata_path}")
|
| 63 |
+
|
| 64 |
+
return output_dir
|
| 65 |
+
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logger.error(f"Failed to save pickle files: {str(e)}")
|
| 68 |
+
raise
|
| 69 |
+
|
| 70 |
+
def save_metrics_to_csv(self, state: TrainingState, output_dir: str):
|
| 71 |
+
observations_dir = os.path.join(output_dir, "observations")
|
| 72 |
+
os.makedirs(observations_dir, exist_ok=True)
|
| 73 |
+
|
| 74 |
+
# 1. Model Comparison Summary
|
| 75 |
+
# ----------------------------------------------------------------------
|
| 76 |
+
metrics_data = []
|
| 77 |
+
for model_name, metrics in state.model_metrics.items():
|
| 78 |
+
metrics_data.append({
|
| 79 |
+
'Model': model_name,
|
| 80 |
+
'Accuracy': metrics['accuracy'],
|
| 81 |
+
'Precision': metrics['precision'],
|
| 82 |
+
'Recall': metrics['recall'],
|
| 83 |
+
'F1_Score': metrics['f1_score'],
|
| 84 |
+
'CV_Score': metrics.get('best_cv_score', 'N/A'),
|
| 85 |
+
'Is_Best_Model': '1' if model_name == state.best_model_name else '0'
|
| 86 |
+
})
|
| 87 |
+
|
| 88 |
+
df_summary = pd.DataFrame(metrics_data)
|
| 89 |
+
df_summary = df_summary.sort_values('Accuracy', ascending=False)
|
| 90 |
+
summary_path = os.path.join(observations_dir, "model_comparison_summary.csv")
|
| 91 |
+
df_summary.to_csv(summary_path, index=False)
|
| 92 |
+
logger.info(f"Saved: model_comparison_summary.csv")
|
| 93 |
+
|
| 94 |
+
# 2. Best Parameters for Each Model
|
| 95 |
+
# ----------------------------------------------------------------------
|
| 96 |
+
params_data = []
|
| 97 |
+
for model_name, metrics in state.model_metrics.items():
|
| 98 |
+
best_params = metrics.get('best_params', {})
|
| 99 |
+
if isinstance(best_params, dict):
|
| 100 |
+
params_str = json.dumps(best_params, indent=2)
|
| 101 |
+
else:
|
| 102 |
+
params_str = str(best_params)
|
| 103 |
+
|
| 104 |
+
params_data.append({
|
| 105 |
+
'Model': model_name,
|
| 106 |
+
'Best_Parameters': params_str,
|
| 107 |
+
'CV_Score': metrics.get('best_cv_score', 'N/A')
|
| 108 |
+
})
|
| 109 |
+
|
| 110 |
+
df_params = pd.DataFrame(params_data)
|
| 111 |
+
params_path = os.path.join(observations_dir, "best_parameters.csv")
|
| 112 |
+
df_params.to_csv(params_path, index=False)
|
| 113 |
+
logger.info(f"Saved: best_parameters.csv")
|
| 114 |
+
|
| 115 |
+
# 3. Cross-Validation Results Summary
|
| 116 |
+
# ----------------------------------------------------------------------
|
| 117 |
+
if state.cv_results:
|
| 118 |
+
cv_summary = []
|
| 119 |
+
for model_name, cv_data in state.cv_results.items():
|
| 120 |
+
cv_summary.append({
|
| 121 |
+
'Model': model_name,
|
| 122 |
+
'Best_CV_Score': cv_data.get('best_score', 'N/A'),
|
| 123 |
+
'Best_Parameters': json.dumps(cv_data.get('best_params', {}))
|
| 124 |
+
})
|
| 125 |
+
|
| 126 |
+
df_cv = pd.DataFrame(cv_summary)
|
| 127 |
+
cv_path = os.path.join(observations_dir, "cross_validation_summary.csv")
|
| 128 |
+
df_cv.to_csv(cv_path, index=False)
|
| 129 |
+
logger.info(f"Saved: cross_validation_summary.csv")
|
| 130 |
+
|
| 131 |
+
# 4. Best Model Information
|
| 132 |
+
# ----------------------------------------------------------------------
|
| 133 |
+
best_model_info = {
|
| 134 |
+
'Attribute': [
|
| 135 |
+
'Best Model Name',
|
| 136 |
+
'Accuracy',
|
| 137 |
+
'Precision',
|
| 138 |
+
'Recall',
|
| 139 |
+
'F1-Score',
|
| 140 |
+
'CV Score',
|
| 141 |
+
'Best Parameters'
|
| 142 |
+
],
|
| 143 |
+
'Value': [
|
| 144 |
+
state.best_model_name,
|
| 145 |
+
state.model_metrics[state.best_model_name]['accuracy'],
|
| 146 |
+
state.model_metrics[state.best_model_name]['precision'],
|
| 147 |
+
state.model_metrics[state.best_model_name]['recall'],
|
| 148 |
+
state.model_metrics[state.best_model_name]['f1_score'],
|
| 149 |
+
state.model_metrics[state.best_model_name].get('best_cv_score', 'N/A'),
|
| 150 |
+
json.dumps(state.best_params, indent=2) if isinstance(state.best_params, dict) else str(state.best_params)
|
| 151 |
+
]
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
df_best = pd.DataFrame(best_model_info)
|
| 155 |
+
best_path = os.path.join(observations_dir, "best_model_info.csv")
|
| 156 |
+
df_best.to_csv(best_path, index=False)
|
| 157 |
+
logger.info(f"Saved: best_model_info.csv")
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def train_models(self, state: TrainingState, cv_folds: int = 5) -> TrainingState:
|
| 161 |
+
logger.info("Model training started")
|
| 162 |
+
logger.info(f"Using GridSearchCV with {cv_folds}-fold CV")
|
| 163 |
+
|
| 164 |
+
try:
|
| 165 |
+
X_train = state.X_train_tfidf
|
| 166 |
+
X_test = state.X_test_tfidf
|
| 167 |
+
y_train = state.y_train
|
| 168 |
+
y_test = state.y_test
|
| 169 |
+
|
| 170 |
+
trained_models, model_metrics, cv_results = {}, {}, {}
|
| 171 |
+
|
| 172 |
+
# Define model instances
|
| 173 |
+
models = {
|
| 174 |
+
'LogisticRegression': LogisticRegression(random_state=42),
|
| 175 |
+
'DecisionTree': DecisionTreeClassifier(random_state=42),
|
| 176 |
+
'SVM': SVC(random_state=42),
|
| 177 |
+
'KNN': KNeighborsClassifier(),
|
| 178 |
+
'RandomForest': RandomForestClassifier(random_state=42)
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
for model_name, model in models.items():
|
| 182 |
+
start_time = time.time()
|
| 183 |
+
logger.info(f"\n{'='*60}")
|
| 184 |
+
logger.info(f"Training {model_name}...")
|
| 185 |
+
|
| 186 |
+
param_grid = self.param_grids.get(model_name, {})
|
| 187 |
+
|
| 188 |
+
search = GridSearchCV(model,
|
| 189 |
+
param_grid=param_grid,
|
| 190 |
+
cv=cv_folds,
|
| 191 |
+
scoring='f1',
|
| 192 |
+
n_jobs=-1
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
search.fit(X_train, y_train)
|
| 196 |
+
best_model = search.best_estimator_
|
| 197 |
+
|
| 198 |
+
y_pred = best_model.predict(X_test)
|
| 199 |
+
|
| 200 |
+
metrics = {
|
| 201 |
+
'accuracy': accuracy_score(y_test, y_pred),
|
| 202 |
+
'precision': precision_score(y_test, y_pred, average='weighted', zero_division=0),
|
| 203 |
+
'recall': recall_score(y_test, y_pred, average='weighted', zero_division=0),
|
| 204 |
+
'f1_score': f1_score(y_test, y_pred, average='weighted', zero_division=0),
|
| 205 |
+
'best_params': search.best_params_,
|
| 206 |
+
'best_cv_score': search.best_score_
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
trained_models[model_name] = best_model
|
| 210 |
+
model_metrics[model_name] = metrics
|
| 211 |
+
cv_results[model_name] = {
|
| 212 |
+
'cv_scores': search.cv_results_,
|
| 213 |
+
'best_params': search.best_params_,
|
| 214 |
+
'best_score': search.best_score_
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
end_time = time.time()
|
| 218 |
+
|
| 219 |
+
logger.info(f"{model_name} - Training time: {end_time - start_time:.2f} seconds")
|
| 220 |
+
logger.info(f"{model_name} - Best Parameters: {search.best_params_}")
|
| 221 |
+
logger.info(f"{model_name} - CV Score: {search.best_score_:.4f}")
|
| 222 |
+
logger.info(f"{model_name} - Test Accuracy: {metrics['accuracy']:.4f}")
|
| 223 |
+
logger.info(f"{model_name} - Test Precision: {metrics['precision']:.4f}")
|
| 224 |
+
logger.info(f"{model_name} - Test Recall: {metrics['recall']:.4f}")
|
| 225 |
+
logger.info(f"{model_name} - Test F1-Score: {metrics['f1_score']:.4f}")
|
| 226 |
+
|
| 227 |
+
# Find best model based on F1-score
|
| 228 |
+
best_model_name = max(model_metrics, key=lambda x: model_metrics[x]['f1_score'])
|
| 229 |
+
best_model = trained_models[best_model_name]
|
| 230 |
+
best_params = model_metrics[best_model_name]['best_params']
|
| 231 |
+
|
| 232 |
+
logger.info(f"{'='*60}")
|
| 233 |
+
logger.info(f"BEST MODEL: {best_model_name}")
|
| 234 |
+
logger.info(f"Best F1-Score: {model_metrics[best_model_name]['f1_score']:.4f}")
|
| 235 |
+
logger.info(f"Best Parameters: {best_params}")
|
| 236 |
+
logger.info(f"{'='*60}")
|
| 237 |
+
|
| 238 |
+
state.trained_models = trained_models
|
| 239 |
+
state.model_metrics = model_metrics
|
| 240 |
+
state.best_model_name = best_model_name
|
| 241 |
+
state.best_model = best_model
|
| 242 |
+
state.best_params = best_params
|
| 243 |
+
state.cv_results = cv_results
|
| 244 |
+
|
| 245 |
+
output_dir = self.save_pickle_files(state)
|
| 246 |
+
self.save_metrics_to_csv(state, output_dir)
|
| 247 |
+
logger.info("\nModel training completed successfully")
|
| 248 |
+
logger.info(f"All outputs saved to: {output_dir}/")
|
| 249 |
+
return state
|
| 250 |
+
|
| 251 |
+
except Exception as e:
|
| 252 |
+
logger.error(f"Failed to train models: {str(e)}")
|
| 253 |
+
raise e
|
Version-3/src/config/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (1.91 kB). View file
|
|
|
Version-3/src/config/config.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
|
| 3 |
+
@dataclass
|
| 4 |
+
class Config:
|
| 5 |
+
training_data_path: str = "data/dataset/dataset.csv"
|
| 6 |
+
validation_data_path: str = "data/dataset/All_mail_Including_Spam_and_Trash.mbox"
|
| 7 |
+
OUTPUT_BASE_DIR: str = "outputs"
|
| 8 |
+
model_path: str = "outputs/2025-12-25_14-02-05/models/SVM_model.pkl"
|
| 9 |
+
feature_path: str = "outputs/2025-12-25_14-02-05/models/vectorizer.pkl"
|
| 10 |
+
|
| 11 |
+
class ModelConfig:
|
| 12 |
+
models = {
|
| 13 |
+
'LogisticRegression': {
|
| 14 |
+
'C': [0.01, 0.1, 1, 10, 100],
|
| 15 |
+
'solver': ['lbfgs', 'liblinear'],
|
| 16 |
+
'max_iter': [100, 200, 300]
|
| 17 |
+
},
|
| 18 |
+
'DecisionTree': {
|
| 19 |
+
'criterion': ['gini', 'entropy'],
|
| 20 |
+
'max_depth': [5, 10, 15, 20, None],
|
| 21 |
+
'min_samples_split': [2, 5, 10],
|
| 22 |
+
'min_samples_leaf': [1, 2, 4]
|
| 23 |
+
},
|
| 24 |
+
'SVM': {
|
| 25 |
+
'C': [0.1, 1, 10],
|
| 26 |
+
'kernel': ['linear', 'rbf'],
|
| 27 |
+
'gamma': ['scale', 'auto']
|
| 28 |
+
},
|
| 29 |
+
'KNN': {
|
| 30 |
+
'n_neighbors': [3, 5, 7, 9, 11],
|
| 31 |
+
'weights': ['uniform', 'distance'],
|
| 32 |
+
'metric': ['euclidean', 'manhattan']
|
| 33 |
+
},
|
| 34 |
+
'RandomForest': {
|
| 35 |
+
'n_estimators': [50, 100, 200],
|
| 36 |
+
'max_depth': [10, 20, 30, None],
|
| 37 |
+
'min_samples_split': [2, 5, 10],
|
| 38 |
+
'min_samples_leaf': [1, 2, 4],
|
| 39 |
+
'max_features': ['sqrt', 'log2']
|
| 40 |
+
}
|
| 41 |
+
}
|
Version-3/src/pipeline/__init__.py
ADDED
|
File without changes
|
Version-3/src/pipeline/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (171 Bytes). View file
|
|
|
Version-3/src/pipeline/__pycache__/prediction_pipeline.cpython-313.pyc
ADDED
|
Binary file (7.67 kB). View file
|
|
|
Version-3/src/pipeline/prediction_pipeline.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import mailbox
|
| 2 |
+
import pickle
|
| 3 |
+
import time
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from src.utils.state import PredictionState
|
| 9 |
+
from src.utils.logger import get_logger
|
| 10 |
+
from src.config.config import Config
|
| 11 |
+
from src.utils.email_utils import extract_body, all_recipients, clean_text
|
| 12 |
+
|
| 13 |
+
logger = get_logger(__name__)
|
| 14 |
+
|
| 15 |
+
class PredictionPipeline:
|
| 16 |
+
def __init__(self, load_models: bool = True):
|
| 17 |
+
self.config = Config()
|
| 18 |
+
self.mailbox = None
|
| 19 |
+
self.feature_transformer = None
|
| 20 |
+
self.model = None
|
| 21 |
+
|
| 22 |
+
if load_models:
|
| 23 |
+
self._load_models()
|
| 24 |
+
|
| 25 |
+
def _load_models(self) -> None:
|
| 26 |
+
|
| 27 |
+
logger.info("Loading models...")
|
| 28 |
+
self.feature_transformer = pickle.load(open(self.config.feature_path, "rb"))
|
| 29 |
+
self.model = pickle.load(open(self.config.model_path, "rb"))
|
| 30 |
+
logger.info("Models loaded successfully")
|
| 31 |
+
|
| 32 |
+
def predict_single_email(self, email_body: str) -> Dict:
|
| 33 |
+
if self.model is None or self.feature_transformer is None:
|
| 34 |
+
self._load_models()
|
| 35 |
+
|
| 36 |
+
cleaned_body = clean_text(email_body)
|
| 37 |
+
features = self.feature_transformer.transform([cleaned_body])
|
| 38 |
+
prediction = self.model.predict(features)
|
| 39 |
+
prediction_label = "Spam" if str(prediction[0]) == "0" else "Ham"
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
prediction_proba = self.model.predict_proba(features)
|
| 43 |
+
confidence = float(max(prediction_proba[0])) * 100
|
| 44 |
+
except:
|
| 45 |
+
confidence = None
|
| 46 |
+
|
| 47 |
+
return {
|
| 48 |
+
'prediction': prediction_label,
|
| 49 |
+
'confidence': confidence,
|
| 50 |
+
'raw_prediction': int(prediction[0])
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def load_mailbox(self, mailbox_path: str) -> None:
|
| 54 |
+
"""Load MBOX file"""
|
| 55 |
+
|
| 56 |
+
logger.info(f"Loading mailbox from {mailbox_path}")
|
| 57 |
+
self.mailbox = mailbox.mbox(mailbox_path)
|
| 58 |
+
logger.info(f"Loaded mailbox from {mailbox_path}")
|
| 59 |
+
|
| 60 |
+
def process_mailbox(self, mailbox_path: Optional[str] = None) -> List[Dict]:
|
| 61 |
+
if mailbox_path:
|
| 62 |
+
self.load_mailbox(mailbox_path)
|
| 63 |
+
|
| 64 |
+
if self.mailbox is None:
|
| 65 |
+
raise ValueError("No mailbox loaded. Call load_mailbox() first.")
|
| 66 |
+
|
| 67 |
+
logger.info("Processing mailbox")
|
| 68 |
+
data = []
|
| 69 |
+
|
| 70 |
+
for message in self.mailbox:
|
| 71 |
+
labels = (message.get("X-Gmail-Labels") or "").lower()
|
| 72 |
+
category = (
|
| 73 |
+
"Spam" if "spam" in labels else
|
| 74 |
+
"Promotions" if "category_promotions" in labels else
|
| 75 |
+
"Social" if "category_social" in labels else
|
| 76 |
+
"Updates" if "category_updates" in labels else
|
| 77 |
+
"Inbox"
|
| 78 |
+
)
|
| 79 |
+
time_str = message.get("Date", "")
|
| 80 |
+
recipients = clean_text(all_recipients(message))
|
| 81 |
+
subject = clean_text(message.get("Subject", ""))
|
| 82 |
+
body = clean_text(extract_body(message))
|
| 83 |
+
direction = "Sent" if "Sent" in (message.get("X-Gmail-Labels") or "") else "Received"
|
| 84 |
+
|
| 85 |
+
data.append({
|
| 86 |
+
"Time": time_str,
|
| 87 |
+
"Recipients": recipients,
|
| 88 |
+
"Subject": subject,
|
| 89 |
+
"Body": body,
|
| 90 |
+
"Category": category,
|
| 91 |
+
"Direction": direction
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
logger.info(f"Processed {len(data)} emails from mailbox")
|
| 95 |
+
self.mailbox.close()
|
| 96 |
+
|
| 97 |
+
return data
|
| 98 |
+
|
| 99 |
+
def run_prediction(self, mail_data: List[Dict]) -> List[Dict]:
|
| 100 |
+
if self.model is None or self.feature_transformer is None:
|
| 101 |
+
self._load_models()
|
| 102 |
+
|
| 103 |
+
start_time = time.time()
|
| 104 |
+
logger.info("Running predictions")
|
| 105 |
+
|
| 106 |
+
for mail in mail_data:
|
| 107 |
+
body_text = mail.get('Body', '')
|
| 108 |
+
features = self.feature_transformer.transform([body_text])
|
| 109 |
+
prediction = self.model.predict(features)
|
| 110 |
+
prediction_label = "Spam" if str(prediction[0]) == "0" else "Ham"
|
| 111 |
+
mail["Prediction"] = prediction_label
|
| 112 |
+
|
| 113 |
+
end_time = time.time()
|
| 114 |
+
logger.info(f"Prediction completed in {end_time - start_time:.2f} seconds")
|
| 115 |
+
|
| 116 |
+
return mail_data
|
| 117 |
+
|
| 118 |
+
def predict_mbox_file(self, mailbox_path: str, output_path: Optional[str] = None) -> pd.DataFrame:
|
| 119 |
+
mail_data = self.process_mailbox(mailbox_path)
|
| 120 |
+
mail_data = self.run_prediction(mail_data)
|
| 121 |
+
df = pd.DataFrame(mail_data)
|
| 122 |
+
if output_path:
|
| 123 |
+
df.to_csv(output_path, index=False)
|
| 124 |
+
logger.info(f"Predictions saved to {output_path}")
|
| 125 |
+
return df
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def run_legacy_pipeline(state: PredictionState) -> None:
|
| 129 |
+
pipeline = PredictionPipeline(load_models=False)
|
| 130 |
+
pipeline.load_mailbox(state.mailbox_path)
|
| 131 |
+
mail_data = pipeline.process_mailbox()
|
| 132 |
+
state.mail_data = mail_data
|
| 133 |
+
state.mail_data = pipeline.run_prediction(state.mail_data)
|
| 134 |
+
df = pd.DataFrame(state.mail_data)
|
| 135 |
+
df.to_csv("data/predictions.csv", index=False)
|
Version-3/src/pipeline/training_pipeline.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.components.data_ingestion import DataIngestion
|
| 2 |
+
from src.components.data_transformation import DataTransformation
|
| 3 |
+
from src.components.model_training import ModelTraining
|
| 4 |
+
from src.utils.state import TrainingState
|
| 5 |
+
from src.utils.logger import get_logger
|
| 6 |
+
|
| 7 |
+
logger = get_logger(__name__)
|
| 8 |
+
|
| 9 |
+
class TrainingPipeline:
|
| 10 |
+
"""Complete training pipeline for spam classification"""
|
| 11 |
+
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.state = TrainingState()
|
| 14 |
+
|
| 15 |
+
def run_pipeline(self, cv_folds: int = 5):
|
| 16 |
+
try:
|
| 17 |
+
logger.info("Initiating training pipeline")
|
| 18 |
+
ingestion = DataIngestion()
|
| 19 |
+
self.state = ingestion.load_data(self.state)
|
| 20 |
+
logger.info(f"Data loaded successfully: {self.state.training_data.shape}")
|
| 21 |
+
logger.info(f"Columns: {self.state.training_data.columns.tolist()}")
|
| 22 |
+
logger.info(f"Sample size: {len(self.state.training_data)} emails")
|
| 23 |
+
|
| 24 |
+
transformation = DataTransformation()
|
| 25 |
+
self.state = transformation.transform_data(self.state)
|
| 26 |
+
logger.info(f"Data transformation completed")
|
| 27 |
+
logger.info(f"Training set: {len(self.state.X_train)} samples")
|
| 28 |
+
logger.info(f"Test set: {len(self.state.X_test)} samples")
|
| 29 |
+
logger.info(f"TF-IDF features: {self.state.X_train_tfidf.shape[1]}")
|
| 30 |
+
|
| 31 |
+
trainer = ModelTraining()
|
| 32 |
+
self.state = trainer.train_models(
|
| 33 |
+
self.state,
|
| 34 |
+
cv_folds=cv_folds
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
logger.info("\n" + "="*70)
|
| 38 |
+
logger.info("Training pipeline completed successfully")
|
| 39 |
+
logger.info("="*70)
|
| 40 |
+
logger.info(f"All metrics saved to 'results/' directory")
|
| 41 |
+
logger.info(f"Best model: {self.state.best_model_name}")
|
| 42 |
+
logger.info(f"Best F1-Score: {self.state.model_metrics[self.state.best_model_name]['f1_score']:.4f}")
|
| 43 |
+
|
| 44 |
+
return self.state
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
logger.error(f"Pipeline failed: {str(e)}")
|
| 48 |
+
raise e
|
| 49 |
+
|
| 50 |
+
if __name__ == "__main__":
|
| 51 |
+
pipeline = TrainingPipeline()
|
| 52 |
+
pipeline.run_pipeline(cv_folds=5)
|
Version-3/src/utils/__init__.py
ADDED
|
File without changes
|
Version-3/src/utils/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (168 Bytes). View file
|
|
|
Version-3/src/utils/__pycache__/email_utils.cpython-313.pyc
ADDED
|
Binary file (2.94 kB). View file
|
|
|
Version-3/src/utils/__pycache__/logger.cpython-313.pyc
ADDED
|
Binary file (1.58 kB). View file
|
|
|
Version-3/src/utils/__pycache__/state.cpython-313.pyc
ADDED
|
Binary file (2.02 kB). View file
|
|
|
Version-3/src/utils/email_utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from html import unescape
|
| 3 |
+
from email.utils import getaddresses
|
| 4 |
+
from bs4 import BeautifulSoup
|
| 5 |
+
|
| 6 |
+
# ----------------------------------------------------------------------------
|
| 7 |
+
# Function to extract email body content
|
| 8 |
+
# ----------------------------------------------------------------------------
|
| 9 |
+
def extract_body(msg):
|
| 10 |
+
texts = []
|
| 11 |
+
|
| 12 |
+
if msg.is_multipart():
|
| 13 |
+
for part in msg.walk():
|
| 14 |
+
if part.get_content_type() in ("text/plain", "text/html"):
|
| 15 |
+
payload = part.get_payload(decode=True)
|
| 16 |
+
if payload:
|
| 17 |
+
text = payload.decode(errors="ignore")
|
| 18 |
+
text = unescape(text)
|
| 19 |
+
text = BeautifulSoup(text, "html.parser").get_text(" ")
|
| 20 |
+
texts.append(text)
|
| 21 |
+
else:
|
| 22 |
+
payload = msg.get_payload(decode=True)
|
| 23 |
+
if payload:
|
| 24 |
+
text = unescape(payload.decode(errors="ignore"))
|
| 25 |
+
text = BeautifulSoup(text, "html.parser").get_text(" ")
|
| 26 |
+
texts.append(text)
|
| 27 |
+
|
| 28 |
+
clean = " ".join(texts)
|
| 29 |
+
clean = re.sub(r'\\+', ' ', clean)
|
| 30 |
+
clean = re.sub(r'[\r\n\t]+', ' ', clean)
|
| 31 |
+
clean = re.sub(r'\s+', ' ', clean)
|
| 32 |
+
return clean.strip()
|
| 33 |
+
|
| 34 |
+
# ----------------------------------------------------------------------------
|
| 35 |
+
# Function to extract all recipients from email headers
|
| 36 |
+
# ----------------------------------------------------------------------------
|
| 37 |
+
def all_recipients(msg):
|
| 38 |
+
fields = []
|
| 39 |
+
for h in ["From", "To", "Cc", "Bcc"]:
|
| 40 |
+
fields.extend(getaddresses([msg.get(h, "")]))
|
| 41 |
+
return ", ".join(sorted(set(addr for _, addr in fields if addr)))
|
| 42 |
+
|
| 43 |
+
# ----------------------------------------------------------------------------
|
| 44 |
+
# Function to clean text for Excel compatibility
|
| 45 |
+
# ----------------------------------------------------------------------------
|
| 46 |
+
def clean_text(text):
|
| 47 |
+
if not isinstance(text, str):
|
| 48 |
+
return text
|
| 49 |
+
text = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\u200B\u200C\u200D\u200E\u200F\uFEFF]', '', text)
|
| 50 |
+
text = text.encode("utf-16", "surrogatepass").decode("utf-16", "ignore")
|
| 51 |
+
text = text[:32767]
|
| 52 |
+
if text.startswith(("=", "+", "-", "@")):
|
| 53 |
+
text = "'" + text
|
| 54 |
+
return text
|
Version-3/src/utils/logger.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
# Global variable to store the log file path for the current run
|
| 6 |
+
_LOG_FILE = None
|
| 7 |
+
|
| 8 |
+
def get_logger(name: str):
|
| 9 |
+
global _LOG_FILE
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(name)
|
| 12 |
+
logger.setLevel(logging.INFO)
|
| 13 |
+
|
| 14 |
+
if logger.handlers:
|
| 15 |
+
return logger
|
| 16 |
+
|
| 17 |
+
# Create log file path only once for the entire pipeline run
|
| 18 |
+
if _LOG_FILE is None:
|
| 19 |
+
date_dir = datetime.now().strftime("%Y-%m-%d")
|
| 20 |
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
| 21 |
+
log_dir = Path("logs") / date_dir
|
| 22 |
+
log_dir.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
_LOG_FILE = log_dir / f"{timestamp}.log"
|
| 24 |
+
|
| 25 |
+
handler = logging.FileHandler(_LOG_FILE, encoding="utf-8")
|
| 26 |
+
formatter = logging.Formatter(
|
| 27 |
+
"[%(asctime)s]: %(filename)s - Line %(lineno)d: %(levelname)s: %(message)s",
|
| 28 |
+
datefmt="%Y-%m-%d %H:%M:%S"
|
| 29 |
+
)
|
| 30 |
+
handler.setFormatter(formatter)
|
| 31 |
+
|
| 32 |
+
logger.addHandler(handler)
|
| 33 |
+
logger.propagate = False
|
| 34 |
+
return logger
|
Version-3/src/utils/state.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional, List, Dict, Any
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
class TrainingState:
|
| 5 |
+
training_data_path: Optional[str] = None
|
| 6 |
+
training_data: Optional[pd.DataFrame] = None
|
| 7 |
+
transformed_data: Optional[pd.DataFrame] = None
|
| 8 |
+
X_train: Optional[pd.Series] = None
|
| 9 |
+
X_test: Optional[pd.Series] = None
|
| 10 |
+
y_train: Optional[pd.Series] = None
|
| 11 |
+
y_test: Optional[pd.Series] = None
|
| 12 |
+
X_train_tfidf: Optional[Any] = None
|
| 13 |
+
X_test_tfidf: Optional[Any] = None
|
| 14 |
+
tfidf_vectorizer: Optional[Any] = None
|
| 15 |
+
trained_models: Optional[Dict[str, Any]] = None
|
| 16 |
+
model_metrics: Optional[Dict[str, Dict[str, float]]] = None
|
| 17 |
+
best_model_name: Optional[str] = None
|
| 18 |
+
best_model: Optional[Any] = None
|
| 19 |
+
best_params: Optional[Dict[str, Any]] = None
|
| 20 |
+
cv_results: Optional[Dict[str, Any]] = None
|
| 21 |
+
|
| 22 |
+
class PredictionState:
|
| 23 |
+
mailbox_path: Optional[str] = None
|
| 24 |
+
mail_data: Optional[List[Dict[str, str]]] = None
|
Version-3/src/utils/utils.py
ADDED
|
File without changes
|