Files
labs/Lab-4/Lab-4-Practical/01_SMS_Dataset_Inspection_Lab.ipynb
T

6.7 KiB

Module 4 — Lab 1: Inspecting the SMS Spam Dataset

Topics: text data, duplicates, conflicting labels, class distribution

Lab version: Complete every TODO, run the cells in order, and use the self-check before opening the Solution.

Learning objectives

After completing this lab, you will be able to:

  • load the original UCI tab-separated text file;
  • audit missing values, exact duplicates, and conflicting labels;
  • create a binary target without modifying the message text;
  • describe class imbalance and message-length differences;
  • explain why aggressive cleaning may remove useful security signals.
In [ ]:
from pathlib import Path
import pandas as pd

DATA_PATH = Path("data/SMSSpamCollection")

if not DATA_PATH.exists():
    raise FileNotFoundError(
        "Missing data/SMSSpamCollection. Follow data/README.md to download "
        "and extract the official UCI SMS Spam Collection."
    )

1. Load the original collection

The source file has no header. Each row contains a label, one tab character, and the message text.

In [ ]:
# TODO 1: load the tab-separated UCI file into raw_df.
# Use the column names label and message.
raw_df = None

# TODO 2: display the shape, first five rows, and raw label counts.
print("Raw shape:", None)
raw_df

2. Audit the records

Exact duplicates can cross a random train-test split and make evaluation too optimistic. A more serious problem occurs when identical text has different labels. Audit both conditions before modeling.

In [ ]:
# TODO 3: normalize label whitespace and remove rows with missing or empty text.
audited_df = None

# TODO 4: count exact duplicate rows.
exact_duplicate_count = None

# TODO 5: find messages associated with more than one distinct label.
conflicting_messages = None

print("Exact duplicate rows:", exact_duplicate_count)
print("Conflicting message texts:", None)

3. Build the clean classroom table

Remove ambiguous messages before exact duplicates, then map ham → 0 and spam → 1. Keep the original text intact.

In [ ]:
# TODO 6: remove every conflicting message and then remove exact duplicates.
clean_df = None

# TODO 7: map ham to 0 and spam to 1 in a new target column.
clean_df["target"] = None

print("Clean shape:", None)
print(clean_df["target"].value_counts().sort_index())

4. Describe the classes and message lengths

Length is used only for exploration. The later models will learn from the message text itself.

In [ ]:
# TODO 8: create character_count and word_count columns.
clean_df["character_count"] = None
clean_df["word_count"] = None

# TODO 9: calculate a summary indexed by label with count and median lengths.
length_summary = None
class_distribution = None

print(length_summary)
print("Class proportions:")
print(class_distribution)

Security note: do not over-clean

URLs, phone numbers, currency symbols, punctuation, unusual capitalization, and deliberate misspellings may contain spam evidence. Begin with the vectorizer defaults. Add normalization only when an experiment demonstrates that it improves validation performance without hiding operationally important signals.

Self-check

In [ ]:
assert list(raw_df.columns) == ["label", "message"]
assert clean_df["message"].notna().all()
assert clean_df["message"].ne("").all()
assert set(clean_df["label"].unique()) == {"ham", "spam"}
assert set(clean_df["target"].unique()) == {0, 1}
assert not clean_df.duplicated(subset=["label", "message"]).any()
assert not clean_df["message"].isin(conflicting_messages).any()
assert class_distribution.loc["ham"] > class_distribution.loc["spam"]
assert {"character_count", "word_count"}.issubset(clean_df.columns)
print("✓ Lab 1 completed")

Reflection questions

  1. Why should exact duplicates be removed before the train-test split?
  2. Why might removing URLs and currency symbols reduce detection quality?
  3. Why is accuracy alone risky for this dataset?