23 KiB
23 KiB
In [1]:
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"]In [2]:
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]
assert list(train_df.columns) == list(test_df.columns)
print("Train shape:", train_df.shape)
print("Test shape:", test_df.shape)
print("Model features:", len(feature_cols))
display(train_df.head())Train shape: (125973, 45) Test shape: (22544, 45) Model features: 41
| duration | protocol_type | service | flag | src_bytes | dst_bytes | land | wrong_fragment | urgent | hot | ... | dst_host_same_src_port_rate | dst_host_srv_diff_host_rate | dst_host_serror_rate | dst_host_srv_serror_rate | dst_host_rerror_rate | dst_host_srv_rerror_rate | attack_label | attack_group | binary_label | difficulty | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | tcp | ftp_data | sf | 491 | 0 | 0 | 0 | 0 | 0 | ... | 0.17 | 0.00 | 0.00 | 0.00 | 0.05 | 0.00 | normal | Normal | 0 | 20 |
| 1 | 0 | udp | other | sf | 146 | 0 | 0 | 0 | 0 | 0 | ... | 0.88 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | normal | Normal | 0 | 15 |
| 2 | 0 | tcp | private | s0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0.00 | 0.00 | 1.00 | 1.00 | 0.00 | 0.00 | neptune | DoS | 1 | 19 |
| 3 | 0 | tcp | http | sf | 232 | 8153 | 0 | 0 | 0 | 0 | ... | 0.03 | 0.04 | 0.03 | 0.01 | 0.00 | 0.01 | normal | Normal | 0 | 21 |
| 4 | 0 | tcp | http | sf | 199 | 420 | 0 | 0 | 0 | 0 | ... | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | normal | Normal | 0 | 21 |
5 rows × 45 columns
In [3]:
train_binary = train_df["binary_label"].value_counts().sort_index()
test_binary = test_df["binary_label"].value_counts().sort_index()
train_groups = train_df["attack_group"].value_counts()
test_groups = test_df["attack_group"].value_counts()
train_attack_labels = set(train_df.loc[train_df["binary_label"] == 1, "attack_label"])
test_attack_labels = set(test_df.loc[test_df["binary_label"] == 1, "attack_label"])
unseen_test_attacks = sorted(test_attack_labels - train_attack_labels)
display(pd.DataFrame({"train": train_binary, "test": test_binary}).fillna(0).astype(int))
display(pd.DataFrame({"train": train_groups, "test": test_groups}).fillna(0).astype(int))
print("Attack labels seen only in test:", unseen_test_attacks)| train | test | |
|---|---|---|
| binary_label | ||
| 0 | 67343 | 9711 |
| 1 | 58630 | 12833 |
| train | test | |
|---|---|---|
| attack_group | ||
| DoS | 45927 | 7460 |
| Normal | 67343 | 9711 |
| Probe | 11656 | 2421 |
| R2L | 995 | 2885 |
| U2R | 52 | 67 |
Attack labels seen only in test: ['apache2', 'httptunnel', 'mailbomb', 'mscan', 'named', 'processtable', 'ps', 'saint', 'sendmail', 'snmpgetattack', 'snmpguess', 'sqlattack', 'udpstorm', 'worm', 'xlock', 'xsnoop', 'xterm']
In [4]:
quality_summary = pd.DataFrame({
"train_missing": train_df[feature_cols].isna().sum(),
"test_missing": test_df[feature_cols].isna().sum(),
"train_unique": train_df[feature_cols].nunique(dropna=True),
"test_unique": test_df[feature_cols].nunique(dropna=True),
})
duplicate_counts = {
"train_exact_duplicates": int(train_df.duplicated().sum()),
"test_exact_duplicates": int(test_df.duplicated().sum()),
}
forbidden_model_columns = sorted(set(TARGET_COLUMNS) & set(train_df.columns))
print(duplicate_counts)
print("Columns excluded from modeling:", forbidden_model_columns)
display(quality_summary.sort_values("train_unique").head(10)){'train_exact_duplicates': 0, 'test_exact_duplicates': 0}
Columns excluded from modeling: ['attack_group', 'attack_label', 'binary_label', 'difficulty']
| train_missing | test_missing | train_unique | test_unique | |
|---|---|---|---|---|
| num_outbound_cmds | 0 | 0 | 1 | 1 |
| is_host_login | 0 | 0 | 2 | 2 |
| is_guest_login | 0 | 0 | 2 | 2 |
| land | 0 | 0 | 2 | 2 |
| logged_in | 0 | 0 | 2 | 2 |
| root_shell | 0 | 0 | 2 | 2 |
| protocol_type | 0 | 0 | 3 | 3 |
| wrong_fragment | 0 | 0 | 3 | 3 |
| num_shells | 0 | 0 | 3 | 4 |
| su_attempted | 0 | 0 | 3 | 3 |
In [5]:
assert len(feature_cols) == 41
assert set(CATEGORICAL_FEATURES).issubset(feature_cols)
assert set(train_df["binary_label"].unique()) == {0, 1}
assert set(test_df["attack_group"].unique()).issubset({"Normal", "DoS", "Probe", "R2L", "U2R"})
assert len(unseen_test_attacks) > 0
assert set(feature_cols).isdisjoint(TARGET_COLUMNS)
assert quality_summary.shape[0] == 41
print("✓ Lab 1 completed")✓ Lab 1 completed