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
12 cells3 experiments3 views0 forks

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
Logistic Regression vs MLP | Clusy