Taglish Spam Detection was a final project for CCS312 (Machine Learning), built with my course teammates Ian Placencia and Dominic Vilog under Dr. Gerard Francesco Apolinario. We split the work by approach — Ian built a multilingual transformer (XLM-RoBERTa), Dominic built an LSTM, and I built a classical ML baseline (Logistic Regression + TF-IDF) — then compared all three against each other for detecting spam in Taglish (code-switched Filipino-English) text.
What's presented here is the system I built around that comparison: a unified Flask web app and API serving all three models, an evaluation harness I integrated to benchmark them against a shared test set, and the Docker packaging that ties it together. The results below reflect the full three-way comparison, not just my own model.
To compare all three models fairly, I built a single Flask web app and API that serves Logistic Regression, LSTM, and XLM-RoBERTa side by side — one shared interface instead of three separate scripts. A message goes in once and comes back with all three models' predictions, so the comparison is direct rather than something you'd have to reconstruct from separate outputs.

I integrated an evaluation harness to benchmark all three models against the same held-out test set, producing accuracy, precision, recall, and F1 for each — plus confusion matrices — so the three-way comparison is legible rather than three separate numbers scattered across different scripts.


Before training, the raw data needed cleaning — inconsistent label columns and encodings standardized, then text normalized (URL stripping, punctuation removal, stemming) across both English and Filipino sources. The pipeline isn't perfect: the stemmer is English-trained, so it over-applies to Tagalog words (kapos → kapo, bukas → buka) — a real tradeoff of using a classical NLP pipeline on mixed-language text.

Lane 2 normalizes heterogeneous spam/ham data and evaluates models offline. The normalized final_spam_ham_dataset.csv is used to train all three models. In Lane 1, the live Flask API loads the trained artifacts, serving them alongside each other in memory to compare their predictions simultaneously, outputting the individual results plus a 2-of-3 ensemble consensus.
All three models load once at Flask startup and run in-process rather than as separate services — a single request hits all three in the same call, and I added a 2-of-3 majority-vote ensemble verdict on top of the individual predictions, so the comparison isn't just three numbers side by side but a single derived consensus label.
The raw English and Filipino source datasets came in with inconsistent label columns ("spam", "is_spam", "v1") and unreliable encodings. I built a normalization pipeline with encoding fallbacks and label standardization before any text cleaning happens, so a malformed CSV fails predictably rather than silently corrupting the merged training set.
Diagnosing a sharp drop in LSTM production accuracy led me to a train/serve mismatch: the training script stripped stopwords before vectorization while the live app processed raw input. I updated the evaluation harness to test through the actual production path, so the benchmark reported real recall (37.21%) instead of an inflated validation number (94%) — and surfaced a wider gap: all three models are still benchmarked on pre-cleaned training data, not raw input, so results are internally consistent but not fully representative of production.
As introduced above, what's shown in this case study reflects my own contribution to the project: the unified Flask app serving all three models, the ensemble verdict logic, the dataset normalization pipeline, and the evaluation harness I integrated to benchmark them against a shared test set. Ian's and Dominic's model implementations aren't represented in the code shown here.