Files
labs/Lab-6/Lab-6-Practical/04_OneClass_IsolationForest_Lab.ipynb
T

6.4 KiB

Module 6 — Lab 4: One-Class SVM and Isolation Forest

Topics: normal-only learning, anomaly scores, validation thresholds, PR-AUC, runtime

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:

  • fit two normal-only novelty detectors;
  • standardize features for One-Class SVM;
  • derive a common anomaly-score convention;
  • compare detectors with the same normal-validation alert budget;
  • explain performance and scalability trade-offs.
In [ ]:
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

TRAIN_PATH = Path("data/nsl_kdd_train.csv")
TEST_PATH = Path("data/nsl_kdd_test.csv")

missing_files = [str(path) for path in (TRAIN_PATH, TEST_PATH) if not path.exists()]
if missing_files:
    raise FileNotFoundError(
        "Missing prepared NSL-KDD files: " + ", ".join(missing_files) +
        ". Follow data/README.md and run: python data/prepare_dataset.py"
    )

TARGET_COLUMNS = ["attack_label", "attack_group", "binary_label", "difficulty"]
CATEGORICAL_FEATURES = ["protocol_type", "service", "flag"]


from sklearn.ensemble import IsolationForest
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import OneClassSVM


from sklearn.metrics import (
    accuracy_score, average_precision_score, confusion_matrix,
    f1_score, precision_score, recall_score,
)


def security_metrics(y_true, y_pred):
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()
    return {
        "accuracy": accuracy_score(y_true, y_pred),
        "attack_precision": precision_score(y_true, y_pred, zero_division=0),
        "attack_recall": recall_score(y_true, y_pred, zero_division=0),
        "attack_f1": f1_score(y_true, y_pred, zero_division=0),
        "false_positives": int(fp),
        "false_negatives": int(fn),
        "false_positive_rate": fp / (fp + tn) if fp + tn else 0.0,
        "alerts_per_10000": 10000 * (fp + tp) / len(y_true),
    }
In [ ]:
# TODO 1: load data, define anomaly_features, split normal training rows,
# and cap the normal fit sample at 5,000 records.
train_df = None
test_df = None
anomaly_features = None
normal_fit = None
normal_valid = None
X_test = None
y_test = None

1. Fit the two detectors

In [ ]:
# TODO 2: build StandardScaler pipelines for OneClassSVM and IsolationForest.
detectors = None

# TODO 3: fit both detectors on normal_fit only.
print("Fitted detectors:", None)

2. Apply a common validation-threshold policy

In [ ]:
# TODO 4: for each detector, negate decision_function so larger means
# more anomalous, choose the 99th normal-validation percentile, and evaluate.
detector_outputs = None
detector_comparison = None
display(detector_comparison)

3. Compare attack-group recall

In [ ]:
# TODO 5: calculate recall separately for DoS, Probe, R2L, and U2R.
group_recall = None
display(group_recall)

Self-check

In [ ]:
assert set(detectors) == {"One-Class SVM", "Isolation Forest"}
assert len(normal_fit) <= 5000
assert all(len(output["test_scores"]) == len(test_df) for output in detector_outputs.values())
assert detector_comparison.shape[0] == 2
assert set(group_recall.index) == {"DoS", "Probe", "R2L", "U2R"}
assert detector_comparison["false_positive_rate"].between(0, 1).all()
print("✓ Lab 4 completed")

Reflection questions

  1. Why is One-Class SVM trained on a capped sample?
  2. Why convert both model outputs to a common anomaly-score direction?
  3. Why should per-group recall accompany aggregate attack recall?