8.4 KiB
8.4 KiB
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.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.tree import DecisionTreeClassifier, export_text
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 [ ]:
train_df = pd.read_csv(TRAIN_PATH, low_memory=False)
test_df = pd.read_csv(TEST_PATH, low_memory=False)
feature_cols = [column for column in train_df.columns if column not in TARGET_COLUMNS]
numeric_features = [column for column in feature_cols if column not in CATEGORICAL_FEATURES]
X_train, y_train = train_df[feature_cols], train_df["binary_label"].astype(int)
X_test, y_test = test_df[feature_cols], test_df["binary_label"].astype(int)
preprocessor = ColumnTransformer([
("categorical", OneHotEncoder(handle_unknown="ignore"), CATEGORICAL_FEATURES),
("numeric", SimpleImputer(strategy="median"), numeric_features),
])
tree_pipeline = Pipeline([
("preprocessor", preprocessor),
("tree", DecisionTreeClassifier(
max_depth=8, min_samples_leaf=5, class_weight="balanced", random_state=42
)),
])
tree_pipeline.fit(X_train, y_train)In [ ]:
tree_predictions = tree_pipeline.predict(X_test)
tree_probabilities = tree_pipeline.predict_proba(X_test)[:, 1]
tree_metrics = security_metrics(y_test, tree_predictions)
tree_metrics["pr_auc"] = average_precision_score(y_test, tree_probabilities)
display(pd.Series(tree_metrics, name="Decision Tree"))
group_rows = []
for group in ["DoS", "Probe", "R2L", "U2R"]:
mask = test_df["attack_group"].eq(group).to_numpy()
group_rows.append({
"attack_group": group,
"support": int(mask.sum()),
"recall": float(tree_predictions[mask].mean()) if mask.any() else np.nan,
})
group_recall = pd.DataFrame(group_rows).set_index("attack_group")
display(group_recall.round(4))In [ ]:
fitted_preprocessor = tree_pipeline.named_steps["preprocessor"]
fitted_tree = tree_pipeline.named_steps["tree"]
transformed_names = fitted_preprocessor.get_feature_names_out()
importance = pd.Series(
fitted_tree.feature_importances_, index=transformed_names, name="importance"
).sort_values(ascending=False)
top_importance = importance.head(12)
display(top_importance.to_frame())
rule_text = export_text(fitted_tree, feature_names=list(transformed_names), max_depth=3)
print(rule_text)
ax = top_importance.sort_values().plot(kind="barh", figsize=(9, 6), color="#4C78A8")
ax.set_title("Top Decision Tree feature importances")
ax.set_xlabel("Mean decrease in impurity")
plt.tight_layout()
plt.show()In [ ]:
assert tree_pipeline.named_steps["tree"].get_depth() <= 8
assert len(tree_predictions) == len(test_df)
assert set(tree_predictions).issubset({0, 1})
assert set(group_recall.index) == {"DoS", "Probe", "R2L", "U2R"}
assert isinstance(rule_text, str) and "class:" in rule_text
assert np.isclose(importance.sum(), 1.0)
assert len(top_importance) <= 12
print("✓ Lab 5 completed")