Files
labs/Lab-3/Lab-3-Solution/02_Clean_Split_No_Leakage_Solution.ipynb
T

5.6 KiB

Module 3 — Lab 2: Clean, Split, and Prevent Data Leakage

Topics: duplicates, missing values, train-test split, imputation, standardization, leakage

Solution version: This notebook contains the complete solution. Run the cells in order and compare the results with your Lab notebook.

Learning objectives

After completing this lab, you will be able to:

  • remove duplicate records and normalize invalid numeric values;
  • create a stratified train-test split;
  • fit imputation and scaling operations on training data only;
  • explain how fitting preprocessing on the test set causes leakage.
In [ ]:
from pathlib import Path
import numpy as np
import pandas as pd

DATA_PATH = Path("data/cicids2017_binary_sample.csv")
RANDOM_STATE = 42

from sklearn.model_selection import train_test_split
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler

1. Load and clean the raw table

In [ ]:
raw_df = pd.read_csv(DATA_PATH)
raw_df.columns = raw_df.columns.str.strip()
clean_df = raw_df.drop_duplicates().copy()
clean_df = clean_df.replace([np.inf, -np.inf], np.nan)

print("Raw rows:", len(raw_df))
print("Clean rows:", len(clean_df))
print("Missing numeric values:", int(clean_df.select_dtypes(include="number").isna().sum().sum()))

2. Create features, target, and a stratified split

In [ ]:
X = clean_df.drop(columns="Label").select_dtypes(include="number")
y = clean_df["Label"].astype(str).str.strip().str.upper().ne("BENIGN").astype(int)

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=RANDOM_STATE,
    stratify=y,
)

print("Train shape:", X_train.shape)
print("Test shape:", X_test.shape)
print("Train attack rate:", round(y_train.mean(), 3))
print("Test attack rate:", round(y_test.mean(), 3))

3. Fit preprocessing on training data only

In [ ]:
imputer = SimpleImputer(strategy="median")
scaler = StandardScaler()

X_train_imputed = imputer.fit_transform(X_train)
X_test_imputed = imputer.transform(X_test)

X_train_scaled = scaler.fit_transform(X_train_imputed)
X_test_scaled = scaler.transform(X_test_imputed)

print("Mean of scaled training columns:", np.round(X_train_scaled.mean(axis=0), 3))
print("Mean of scaled test columns:", np.round(X_test_scaled.mean(axis=0), 3))

Why the test mean is not exactly zero

StandardScaler learned its mean and standard deviation from the training set. The test set is transformed with those same values. A non-zero test mean is expected and is evidence that the test set was not used to fit the scaler.

Self-check

In [ ]:
assert len(clean_df) <= len(raw_df)
assert np.isfinite(X_train_scaled).all()
assert np.isfinite(X_test_scaled).all()
assert abs(y_train.mean() - y_test.mean()) < 0.02
assert np.allclose(X_train_scaled.mean(axis=0), 0.0, atol=1e-10)
assert X_train_scaled.shape[1] == X.shape[1]
print("✓ Lab 2 completed")

Reflection questions

  1. Why must the imputer learn medians from the training set only?
  2. What information would leak if the scaler were fit before the train-test split?
  3. Why is stratification useful for an imbalanced security target?