Logistic Regression vs MLP
By Eldar · Published July 7, 2026
Compares logistic regression and neural network architectures on the Enron spam dataset using TF-IDF features, evaluating accuracy, precision, recall, F1, and ROC-AUC.
- spam-detection
- text-classification
- nlp
- tfidf
- model-comparison
- sklearn
Inside this notebook
# Spam Email Classifier — Architecture Comparison **Dataset:** Enron Spam Dataset (~33,700 real emails, ~17k spam / ~16.5k ham, sourced from the Enron Corpus and preprocessed by [Marcel Wiechmann](https://www.kaggle.com/datasets/marcelwiechmann/enron-spam-data)). **Goal:** Compare two simple classifier architectures head-to-head on the same TF-IDF features. | Architecture | Description | |---|---| | **A — Logistic Regression** | L2-regularised linear model on TF-IDF features. Fast, interpretable, a rock-solid baseline. | | **B — Simple MLP** | Feed-forward neural network with two hidden layers (256 → 128, ReLU activations) on the same TF-IDF features. Captures non-linear patterns. | **Comparison dimensions:** Accuracy, Precision, Recall, F1, ROC-AUC, training time, and confusion matrices.
# ── Imports ──────────────────────────────────────────────
%pip install kagglehub -q
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import time
import warnings
warnings.filterwarnings('ignore')
# ML / NLP
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
…Note: you may need to restart the kernel to use updated packages. ✅ All imports ready.
# ── Download Enron Spam Data from Kaggle ──────────────────
import kagglehub
path = kagglehub.dataset_download("marcelwiechmann/enron-spam-data")
print(f"Dataset downloaded to: {path}")
df = pd.read_csv(f"{path}/enron_spam_data.csv")
print(f"Shape: {df.shape}")
df.head(3)Downloading to /root/.cache/kagglehub/datasets/marcelwiechmann/enron-spam-data/3.archive... Extracting files... Dataset downloaded to: /root/.cache/kagglehub/datasets/marcelwiechmann/enron-spam-data/versions/3 Shape: (33716, 5)
# ── Quick EDA ────────────────────────────────────────────
print("Columns:", df.columns.tolist())
print("\nDtypes:\n", df.dtypes)
print("\nMissing values:\n", df.isna().sum())
print("\nClass balance:")
print(df['Spam/Ham'].value_counts())
print(f"\nDuplicate rows: {df.duplicated().sum()}")
# Drop rows where Message is null (they have Subject but no body)
print(f"\nRows before drop: {len(df)}")
df = df.dropna(subset=['Message'])
print(f"Rows after dropping null Message: {len(df)}")
# Combine Subject + Message for richer features
df['text'] = df['Subject'].fillna('') + ' ' + df['Message']
# Encode label: spam=1, ham=0
df['label'] = (df['Spam/Ham'] == 'spam').astype(int)
…Columns: ['Unnamed: 0', 'Subject', 'Message', 'Spam/Ham', 'Date'] Dtypes: Unnamed: 0 int64 Subject object Message object Spam/Ham object Date object dtype: object Missing values: Unnamed: 0 0 Subject 0 Message 52 Spam/Ham 0 Date 0 dtype: int64 Class balance: Spam/Ham spam 17171 ham 16545 Name: count, dtype: int64 Duplicate rows: 0 Rows before drop: 33716 Rows after dropping null Message: 33664 Spam count: 17171 / 33664…
# ── Train / Test Split & TF-IDF Vectorization ───────────
# Stratified split to preserve class balance
X_train, X_test, y_train, y_test = train_test_split(
df['text'], df['label'],
test_size=0.2, random_state=SEED, stratify=df['label']
)
print(f"Train size: {len(X_train):,} | Test size: {len(X_test):,}")
print(f"Train spam ratio: {y_train.mean():.1%} | Test spam ratio: {y_test.mean():.1%}")
# TF-IDF: unigrams + bigrams, max 5000 features, remove English stop words
vectorizer = TfidfVectorizer(
max_features=5000,
ngram_range=(1, 2),
stop_words='english',
sublinear_tf=True # use 1 + log(tf)
)
…Train size: 26,931 | Test size: 6,733 Train spam ratio: 51.0% | Test spam ratio: 51.0% TF-IDF shape — Train: (26931, 5000) | Test: (6733, 5000) Vocabulary size: 5,000