In [3]:
# Importing libraries for data handling and analysis
import pandas as pd
# Loading the NSL-KDD training dataset (CSV file in the same folder as this notebook)
train_df = pd.read_csv("kdd_train.csv")
# Displaying the first five rows to inspect the dataset structure
train_df.head()
Out[3]:
| duration | protocol_type | service | flag | src_bytes | dst_bytes | land | wrong_fragment | urgent | hot | ... | dst_host_srv_count | dst_host_same_srv_rate | dst_host_diff_srv_rate | 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 | labels | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | tcp | ftp_data | SF | 491 | 0 | 0 | 0 | 0 | 0 | ... | 25 | 0.17 | 0.03 | 0.17 | 0.00 | 0.00 | 0.00 | 0.05 | 0.00 | normal |
| 1 | 0 | udp | other | SF | 146 | 0 | 0 | 0 | 0 | 0 | ... | 1 | 0.00 | 0.60 | 0.88 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | normal |
| 2 | 0 | tcp | private | S0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 26 | 0.10 | 0.05 | 0.00 | 0.00 | 1.00 | 1.00 | 0.00 | 0.00 | neptune |
| 3 | 0 | tcp | http | SF | 232 | 8153 | 0 | 0 | 0 | 0 | ... | 255 | 1.00 | 0.00 | 0.03 | 0.04 | 0.03 | 0.01 | 0.00 | 0.01 | normal |
| 4 | 0 | tcp | http | SF | 199 | 420 | 0 | 0 | 0 | 0 | ... | 255 | 1.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | normal |
5 rows × 42 columns
In [7]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
In [10]:
import pandas as pd
from pathlib import Path
csvs = list(Path(".").glob("*.csv"))
csv_names = [p.name.lower() for p in csvs]
def find_file(candidates):
for p in csvs:
name = p.name.lower()
if any(c in name for c in candidates):
return p
return None
train_path = find_file(["kddtrain", "train"])
test_path = find_file(["kddtest", "test"])
if train_path is None or test_path is None:
raise FileNotFoundError(
f"Couldn't auto-find train/test CSVs in this folder.\n"
f"CSV files found: {[p.name for p in csvs]}\n"
f"Expected something containing: train (e.g., KDDTrain+) and test (e.g., KDDTest+)."
)
print("Train file:", train_path.name)
print("Test file :", test_path.name)
#NSL-KDD column names (includes 'difficulty' at the end)
columns = [
"duration","protocol_type","service","flag","src_bytes","dst_bytes","land","wrong_fragment",
"urgent","hot","num_failed_logins","logged_in","num_compromised","root_shell","su_attempted",
"num_root","num_file_creations","num_shells","num_access_files","num_outbound_cmds",
"is_host_login","is_guest_login","count","srv_count","serror_rate","srv_serror_rate",
"rerror_rate","srv_rerror_rate","same_srv_rate","diff_srv_rate","srv_diff_host_rate",
"dst_host_count","dst_host_srv_count","dst_host_same_srv_rate","dst_host_diff_srv_rate",
"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",
"label","difficulty"
]
def read_nsl_kdd(path):
# Read first row to detect whether the file already has headers
peek = pd.read_csv(path, nrows=1, header=None)
already_has_header = peek.iloc[0].astype(str).str.contains("protocol_type|service|flag|label", case=False).any()
if already_has_header:
df = pd.read_csv(path) # keep existing headers
else:
df = pd.read_csv(path, header=None)
if df.shape[1] == len(columns):
df.columns = columns
else:
raise ValueError(
f"{path.name} has {df.shape[1]} columns, expected {len(columns)} for NSL-KDD."
)
return df
df_train = read_nsl_kdd(train_path)
df_test = read_nsl_kdd(test_path)
print("df_train shape:", df_train.shape)
print("df_test shape:", df_test.shape)
print("df_train columns (first 10):", list(df_train.columns)[:10])
Train file: kdd_train.csv Test file : kdd_test.csv df_train shape: (125973, 42) df_test shape: (22544, 42) df_train columns (first 10): ['duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot']
In [12]:
# Clean column names (removes hidden spaces that cause KeyError)
df_train.columns = df_train.columns.str.strip()
df_test.columns = df_test.columns.str.strip()
# Quick check (you can screenshot this output)
print("Last 5 train columns:", list(df_train.columns)[-5:])
print("Last 5 test columns :", list(df_test.columns)[-5:])
# Drop difficulty if present (not a feature)
df_train_model = df_train.drop(columns=["difficulty"], errors="ignore")
df_test_model = df_test.drop(columns=["difficulty"], errors="ignore")
# Find the target column safely
target_col = "label" if "label" in df_train_model.columns else df_train_model.columns[-1]
print("Using target column:", target_col)
# Split features/labels
X_train = df_train_model.drop(columns=[target_col])
y_train = df_train_model[target_col]
X_test = df_test_model.drop(columns=[target_col])
y_test = df_test_model[target_col]
print("X_train:", X_train.shape, "y_train:", y_train.shape)
print("X_test :", X_test.shape, "y_test :", y_test.shape)
Last 5 train columns: ['dst_host_serror_rate', 'dst_host_srv_serror_rate', 'dst_host_rerror_rate', 'dst_host_srv_rerror_rate', 'labels'] Last 5 test columns : ['dst_host_serror_rate', 'dst_host_srv_serror_rate', 'dst_host_rerror_rate', 'dst_host_srv_rerror_rate', 'labels'] Using target column: labels X_train: (125973, 41) y_train: (125973,) X_test : (22544, 41) y_test : (22544,)
In [13]:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
categorical_features = ["protocol_type", "service", "flag"]
numerical_features = [col for col in X_train.columns if col not in categorical_features]
preprocessor = ColumnTransformer(
transformers=[
("num", StandardScaler(), numerical_features),
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features)
]
)
print("Preprocessor ready")
Preprocessor ready
In [14]:
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
rf_pipeline = Pipeline(steps=[
("preprocessing", preprocessor),
("model", RandomForestClassifier(
n_estimators=100,
random_state=42,
n_jobs=-1
))
])
rf_pipeline.fit(X_train, y_train)
print("Random Forest trained")
Random Forest trained
In [15]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
rf_model = rf_pipeline.named_steps["model"]
feature_names_num = numerical_features
feature_names_cat = rf_pipeline.named_steps["preprocessing"] \
.named_transformers_["cat"] \
.get_feature_names_out(categorical_features)
feature_names = np.concatenate([feature_names_num, feature_names_cat])
importance_df = pd.DataFrame({
"feature": feature_names,
"importance": rf_model.feature_importances_
}).sort_values(by="importance", ascending=False)
top_features = importance_df.head(15)
plt.figure(figsize=(8, 5))
plt.barh(top_features["feature"], top_features["importance"])
plt.gca().invert_yaxis()
plt.title("Random Forest Feature Importance (Top 15)")
plt.xlabel("Importance Score")
plt.tight_layout()
plt.show()
In [4]:
# Display first 5 rows with only the first 10 columns (cleaner for screenshots)
train_df.iloc[:5, :10]
Out[4]:
| duration | protocol_type | service | flag | src_bytes | dst_bytes | land | wrong_fragment | urgent | hot | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | tcp | ftp_data | SF | 491 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | udp | other | SF | 146 | 0 | 0 | 0 | 0 | 0 |
| 2 | 0 | tcp | private | S0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 0 | tcp | http | SF | 232 | 8153 | 0 | 0 | 0 | 0 |
| 4 | 0 | tcp | http | SF | 199 | 420 | 0 | 0 | 0 | 0 |
In [ ]:
In [5]:
train_df.shape
Out[5]:
(125973, 42)
In [6]:
# Checking the distribution of attack labels in the training dataset
# The label column is the last column in NSL-KDD
label_col = train_df.columns[-1]
# Display class counts
train_df[label_col].value_counts()
Out[6]:
labels normal 67343 neptune 41214 satan 3633 ipsweep 3599 portsweep 2931 smurf 2646 nmap 1493 back 956 teardrop 892 warezclient 890 pod 201 guess_passwd 53 buffer_overflow 30 warezmaster 20 land 18 imap 11 rootkit 10 loadmodule 9 ftp_write 8 multihop 7 phf 4 perl 3 spy 2 Name: count, dtype: int64
In [8]:
# Distribution of protocol types (categorical feature)
protocol_counts = train_df["protocol_type"].value_counts()
protocol_counts
Out[8]:
protocol_type tcp 102689 udp 14993 icmp 8291 Name: count, dtype: int64
In [9]:
# Distribution of service types
service_counts = train_df['service'].value_counts()
service_counts
Out[9]:
service
http 40338
private 21853
domain_u 9043
smtp 7313
ftp_data 6860
...
tftp_u 3
http_8001 2
aol 2
harvest 2
http_2784 1
Name: count, Length: 70, dtype: int64
In [10]:
# Distribution of connection flags
flag_counts = train_df['flag'].value_counts()
flag_counts
Out[10]:
flag SF 74945 S0 34851 REJ 11233 RSTR 2421 RSTO 1562 S1 365 SH 271 S2 127 RSTOS0 103 S3 49 OTH 46 Name: count, dtype: int64
In [11]:
# Assign column names to the NSL-KDD dataset
columns = [
"duration", "protocol_type", "service", "flag", "src_bytes", "dst_bytes",
"land", "wrong_fragment", "urgent", "hot", "num_failed_logins",
"logged_in", "num_compromised", "root_shell", "su_attempted",
"num_root", "num_file_creations", "num_shells", "num_access_files",
"num_outbound_cmds", "is_host_login", "is_guest_login", "count",
"srv_count", "serror_rate", "srv_serror_rate", "rerror_rate",
"srv_rerror_rate", "same_srv_rate", "diff_srv_rate",
"srv_diff_host_rate", "dst_host_count", "dst_host_srv_count",
"dst_host_same_srv_rate", "dst_host_diff_srv_rate",
"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",
"label"
]
train_df.columns = columns
# Separate features and target label
X = train_df.drop("label", axis=1)
y = train_df["label"]
X.head()
Out[11]:
| duration | protocol_type | service | flag | src_bytes | dst_bytes | land | wrong_fragment | urgent | hot | ... | dst_host_count | dst_host_srv_count | dst_host_same_srv_rate | dst_host_diff_srv_rate | 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 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | tcp | ftp_data | SF | 491 | 0 | 0 | 0 | 0 | 0 | ... | 150 | 25 | 0.17 | 0.03 | 0.17 | 0.00 | 0.00 | 0.00 | 0.05 | 0.00 |
| 1 | 0 | udp | other | SF | 146 | 0 | 0 | 0 | 0 | 0 | ... | 255 | 1 | 0.00 | 0.60 | 0.88 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
| 2 | 0 | tcp | private | S0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 255 | 26 | 0.10 | 0.05 | 0.00 | 0.00 | 1.00 | 1.00 | 0.00 | 0.00 |
| 3 | 0 | tcp | http | SF | 232 | 8153 | 0 | 0 | 0 | 0 | ... | 30 | 255 | 1.00 | 0.00 | 0.03 | 0.04 | 0.03 | 0.01 | 0.00 | 0.01 |
| 4 | 0 | tcp | http | SF | 199 | 420 | 0 | 0 | 0 | 0 | ... | 255 | 255 | 1.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
5 rows × 41 columns
In [12]:
X.iloc[:, :10].head()
Out[12]:
| duration | protocol_type | service | flag | src_bytes | dst_bytes | land | wrong_fragment | urgent | hot | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | tcp | ftp_data | SF | 491 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | udp | other | SF | 146 | 0 | 0 | 0 | 0 | 0 |
| 2 | 0 | tcp | private | S0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 0 | tcp | http | SF | 232 | 8153 | 0 | 0 | 0 | 0 |
| 4 | 0 | tcp | http | SF | 199 | 420 | 0 | 0 | 0 | 0 |
In [14]:
# Get distribution of attack labels (last column)
labels = train_df.iloc[:, -1].value_counts()
labels
Out[14]:
label normal 67343 neptune 41214 satan 3633 ipsweep 3599 portsweep 2931 smurf 2646 nmap 1493 back 956 teardrop 892 warezclient 890 pod 201 guess_passwd 53 buffer_overflow 30 warezmaster 20 land 18 imap 11 rootkit 10 loadmodule 9 ftp_write 8 multihop 7 phf 4 perl 3 spy 2 Name: count, dtype: int64
In [15]:
import matplotlib.pyplot as plt
labels.plot(kind="bar", figsize=(10, 5))
plt.title("Distribution of Attack Classes in the NSL-KDD Training Dataset")
plt.xlabel("Attack Type")
plt.ylabel("Number of Records")
plt.tight_layout()
plt.show()
In [16]:
# Identifying the label column (last column in NSL-KDD)
label_column = train_df.columns[-1]
# Separate features and labels
X = train_df.drop(columns=[label_column])
y = train_df[label_column]
# Display basic checks
X.shape, y.shape
Out[16]:
((125973, 41), (125973,))
In [20]:
# Separate features and target label
X = train_df.drop(columns=["label"])
y = train_df["label"]
In [21]:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=y
)
In [16]:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
# Identify categorical and numerical columns
categorical_features = ["protocol_type", "service", "flag"]
numerical_features = [col for col in X_train.columns if col not in categorical_features]
# Build the preprocessing pipeline
preprocessor = ColumnTransformer(
transformers=[
("cat", OneHotEncoder(handle_unknown="ignore"), categorical_features),
("num", StandardScaler(), numerical_features)
]
)
In [23]:
X_train_processed = preprocessor.fit_transform(X_train)
X_train_processed.shape, y_train.shape
Out[23]:
((100778, 121), (100778,))
In [17]:
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
rf_pipeline = Pipeline(steps=[
("preprocessing", preprocessor),
("model", RandomForestClassifier(
n_estimators=100,
random_state=42,
n_jobs=-1
))
])
rf_pipeline.fit(X_train, y_train)
Out[17]:
Pipeline(steps=[('preprocessing',
ColumnTransformer(transformers=[('cat',
OneHotEncoder(handle_unknown='ignore'),
['protocol_type', 'service',
'flag']),
('num', StandardScaler(),
['duration', 'src_bytes',
'dst_bytes', 'land',
'wrong_fragment', 'urgent',
'hot', 'num_failed_logins',
'logged_in',
'num_compromised',
'root_shell', 'su_attempted',
'num_root',
'num_file_creations',
'num_shells',
'num_access_files',
'num_outbound_cmds',
'is_host_login',
'is_guest_login', 'count',
'srv_count', 'serror_rate',
'srv_serror_rate',
'rerror_rate',
'srv_rerror_rate',
'same_srv_rate',
'diff_srv_rate',
'srv_diff_host_rate',
'dst_host_count',
'dst_host_srv_count', ...])])),
('model', RandomForestClassifier(n_jobs=-1, random_state=42))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocessing',
ColumnTransformer(transformers=[('cat',
OneHotEncoder(handle_unknown='ignore'),
['protocol_type', 'service',
'flag']),
('num', StandardScaler(),
['duration', 'src_bytes',
'dst_bytes', 'land',
'wrong_fragment', 'urgent',
'hot', 'num_failed_logins',
'logged_in',
'num_compromised',
'root_shell', 'su_attempted',
'num_root',
'num_file_creations',
'num_shells',
'num_access_files',
'num_outbound_cmds',
'is_host_login',
'is_guest_login', 'count',
'srv_count', 'serror_rate',
'srv_serror_rate',
'rerror_rate',
'srv_rerror_rate',
'same_srv_rate',
'diff_srv_rate',
'srv_diff_host_rate',
'dst_host_count',
'dst_host_srv_count', ...])])),
('model', RandomForestClassifier(n_jobs=-1, random_state=42))])ColumnTransformer(transformers=[('cat', OneHotEncoder(handle_unknown='ignore'),
['protocol_type', 'service', 'flag']),
('num', StandardScaler(),
['duration', 'src_bytes', 'dst_bytes', 'land',
'wrong_fragment', 'urgent', 'hot',
'num_failed_logins', 'logged_in',
'num_compromised', 'root_shell',
'su_attempted', 'num_root',
'num_file_creations', 'num_shells',
'num_access_files', 'num_outbound_cmds',
'is_host_login', 'is_guest_login', 'count',
'srv_count', 'serror_rate', 'srv_serror_rate',
'rerror_rate', 'srv_rerror_rate',
'same_srv_rate', 'diff_srv_rate',
'srv_diff_host_rate', 'dst_host_count',
'dst_host_srv_count', ...])])['protocol_type', 'service', 'flag']
OneHotEncoder(handle_unknown='ignore')
['duration', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot', 'num_failed_logins', 'logged_in', 'num_compromised', 'root_shell', 'su_attempted', 'num_root', 'num_file_creations', 'num_shells', 'num_access_files', 'num_outbound_cmds', 'is_host_login', 'is_guest_login', 'count', 'srv_count', 'serror_rate', 'srv_serror_rate', 'rerror_rate', 'srv_rerror_rate', 'same_srv_rate', 'diff_srv_rate', 'srv_diff_host_rate', 'dst_host_count', 'dst_host_srv_count', 'dst_host_same_srv_rate', 'dst_host_diff_srv_rate', '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']
StandardScaler()
RandomForestClassifier(n_jobs=-1, random_state=42)
In [26]:
# Random Forest pipeline (preprocessing + model) + accuracy on test set
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
rf_pipeline = Pipeline(
steps=[
("preprocessing", preprocessor),
("model", RandomForestClassifier(
n_estimators=100,
random_state=42,
n_jobs=-1
))
]
)
# Train
rf_pipeline.fit(X_train, y_train)
# Predict + evaluate
y_pred_rf = rf_pipeline.predict(X_test)
rf_accuracy = accuracy_score(y_test, y_pred_rf)
print("Random Forest Accuracy:", rf_accuracy)
Random Forest Accuracy: 0.9984123834094066
In [19]:
# Gradient Boosting model inside a pipeline
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score
# Define the Gradient Boosting pipeline
gb_pipeline = Pipeline(
steps=[
("preprocessing", preprocessor),
("model", GradientBoostingClassifier(random_state=42))
]
)
# Train the model
gb_pipeline.fit(X_train, y_train)
# Make predictions on the test set
y_pred_gb = gb_pipeline.predict(X_test)
# Evaluate accuracy
gb_accuracy = accuracy_score(y_test, y_pred_gb)
print("Gradient Boosting Accuracy:", gb_accuracy)
Gradient Boosting Accuracy: 0.9110184528034067
In [28]:
# Random Forest evaluation on the test data
# This section checks how well the trained model performs, not just overall accuracy,
# but also how it handles each attack class.
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
# Use the trained Random Forest pipeline to predict labels for the test set
y_pred_rf = rf_pipeline.predict(X_test)
# Print a detailed breakdown of precision, recall and F1-score for each class
# This is useful because accuracy alone can hide poor performance on rare attacks
print("Random Forest - Classification Report (Test Set)")
print(classification_report(y_test, y_pred_rf))
# Create a confusion matrix to see where the model is making correct
# and incorrect predictions across all classes
cm_rf = confusion_matrix(y_test, y_pred_rf)
# Display the confusion matrix as a visual plot
# This makes it easier to spot which attacks are being confused with others
disp = ConfusionMatrixDisplay(confusion_matrix=cm_rf)
disp.plot(values_format='d')
plt.title("Random Forest - Confusion Matrix (Test Set)")
plt.show()
Random Forest - Classification Report (Test Set)
C:\Users\kelly\anaconda3\Lib\site-packages\sklearn\metrics\_classification.py:1565: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))
C:\Users\kelly\anaconda3\Lib\site-packages\sklearn\metrics\_classification.py:1565: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))
C:\Users\kelly\anaconda3\Lib\site-packages\sklearn\metrics\_classification.py:1565: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))
precision recall f1-score support
back 0.99 1.00 1.00 191
buffer_overflow 1.00 0.67 0.80 6
ftp_write 0.00 0.00 0.00 2
guess_passwd 1.00 1.00 1.00 11
imap 1.00 1.00 1.00 2
ipsweep 1.00 1.00 1.00 720
land 0.75 0.75 0.75 4
loadmodule 0.00 0.00 0.00 2
multihop 0.00 0.00 0.00 1
neptune 1.00 1.00 1.00 8243
nmap 0.99 0.99 0.99 299
normal 1.00 1.00 1.00 13469
phf 0.00 0.00 0.00 1
pod 1.00 0.97 0.99 40
portsweep 1.00 0.99 1.00 586
rootkit 0.00 0.00 0.00 2
satan 1.00 0.99 0.99 727
smurf 1.00 1.00 1.00 529
teardrop 1.00 1.00 1.00 178
warezclient 1.00 0.97 0.99 178
warezmaster 0.75 0.75 0.75 4
accuracy 1.00 25195
macro avg 0.74 0.72 0.73 25195
weighted avg 1.00 1.00 1.00 25195
In [22]:
# Gradient Boosting model
# I’m using this as a direct comparison to Random Forest,
# keeping the same train/test split and preprocessing pipeline
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
# Build the Gradient Boosting pipeline
# Preprocessing is reused so the comparison is fair
gb_pipeline = Pipeline(
steps=[
("preprocessing", preprocessor),
("model", GradientBoostingClassifier(
random_state=42
))
]
)
# Train the Gradient Boosting model
# This fits both the preprocessing and the model together
gb_pipeline.fit(X_train, y_train)
# Make predictions on the test set
y_pred_gb = gb_pipeline.predict(X_test)
# Calculate accuracy
gb_accuracy = accuracy_score(y_test, y_pred_gb)
# Print accuracy so I can screenshot it for the report
print("Gradient Boosting Accuracy:", gb_accuracy)
Gradient Boosting Accuracy: 0.9110184528034067
In [30]:
# Classification report for Gradient Boosting
# This lets me see how the model performs on each attack class,
# which is important because the dataset is highly imbalanced
from sklearn.metrics import classification_report
print("Gradient Boosting - Classification Report (Test Set)")
print(classification_report(y_test, y_pred_gb))
Gradient Boosting - Classification Report (Test Set)
precision recall f1-score support
back 0.99 1.00 1.00 191
buffer_overflow 0.29 0.33 0.31 6
ftp_write 0.00 0.00 0.00 2
guess_passwd 1.00 1.00 1.00 11
imap 1.00 1.00 1.00 2
ipsweep 1.00 0.99 1.00 720
land 0.50 0.25 0.33 4
loadmodule 0.00 0.00 0.00 2
multihop 0.00 0.00 0.00 1
neptune 1.00 1.00 1.00 8243
nmap 0.99 0.99 0.99 299
normal 1.00 1.00 1.00 13469
phf 0.00 0.00 0.00 1
pod 1.00 0.97 0.99 40
portsweep 1.00 0.99 1.00 586
rootkit 0.00 0.00 0.00 2
satan 0.99 0.99 0.99 727
smurf 1.00 1.00 1.00 529
teardrop 1.00 1.00 1.00 178
warezclient 0.99 0.98 0.98 178
warezmaster 1.00 0.75 0.86 4
accuracy 1.00 25195
macro avg 0.70 0.68 0.69 25195
weighted avg 1.00 1.00 1.00 25195
C:\Users\kelly\anaconda3\Lib\site-packages\sklearn\metrics\_classification.py:1565: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))
C:\Users\kelly\anaconda3\Lib\site-packages\sklearn\metrics\_classification.py:1565: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))
C:\Users\kelly\anaconda3\Lib\site-packages\sklearn\metrics\_classification.py:1565: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
_warn_prf(average, modifier, f"{metric.capitalize()} is", len(result))
In [32]:
# Confusion matrix for Gradient Boosting
# Keeping this the same style and size as the Random Forest matrix for consistency
from sklearn.metrics import confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt
# Generate confusion matrix
cm_gb = confusion_matrix(y_test, y_pred_gb)
# Plot confusion matrix (same settings as Random Forest)
plt.figure(figsize=(10, 8)) # smaller so it fits in one screenshot
sns.heatmap(
cm_gb,
annot=True,
fmt="d",
cmap="viridis",
cbar=True
)
plt.xlabel("Predicted label")
plt.ylabel("True label")
plt.title("Gradient Boosting - Confusion Matrix (Test Set)")
plt.tight_layout()
plt.show()
In [21]:
# Smaller confusion matrix for Gradient Boosting
# Adjusted to fit cleanly into one screenshot
from sklearn.metrics import confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt
cm_gb = confusion_matrix(y_test, y_pred_gb)
plt.figure(figsize=(7, 6)) # reduced size
sns.heatmap(
cm_gb,
annot=True,
fmt="d",
cmap="viridis",
cbar=True,
annot_kws={"size": 7} # smaller numbers so it fits
)
plt.xlabel("Predicted label")
plt.ylabel("True label")
plt.title("Gradient Boosting - Confusion Matrix (Test Set)", fontsize=11)
plt.tight_layout()
plt.show()
In [34]:
from sklearn.metrics import accuracy_score
# Random Forest accuracy
rf_accuracy = accuracy_score(y_test, y_pred_rf)
print("Random Forest Accuracy:", rf_accuracy)
# Gradient Boosting accuracy
gb_accuracy = accuracy_score(y_test, y_pred_gb)
print("Gradient Boosting Accuracy:", gb_accuracy)
Random Forest Accuracy: 0.9984123834094066 Gradient Boosting Accuracy: 0.9978964080174638
In [36]:
# Binary class distribution: Normal vs Attack
import matplotlib.pyplot as plt
# I’m creating a binary label:
# - 'normal' stays as 'normal'
# - everything else becomes 'attack'
binary_labels = y_train.apply(lambda x: "normal" if x == "normal" else "attack")
# Count how many samples fall into each group
binary_counts = binary_labels.value_counts()
# Plot the distribution
plt.figure(figsize=(6, 4)) # keep it small so it fits nicely in one screenshot
binary_counts.plot(kind="bar")
# Labels and title (keep simple and consistent with other figures)
plt.title("Binary Class Distribution (Normal vs Attack)")
plt.xlabel("Traffic Type")
plt.ylabel("Number of Samples")
# Rotate labels slightly so they look clean
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
In [37]:
# Attack category distribution (EDA)
# I'm grouping individual attack types into
# broader attack categories to understand
# high-level imbalance in the dataset
import matplotlib.pyplot as plt
# Mapping attack types to categories (NSL-KDD standard)
attack_category_map = {
'normal': 'Normal',
# DoS attacks
'back': 'DoS', 'land': 'DoS', 'neptune': 'DoS', 'pod': 'DoS',
'smurf': 'DoS', 'teardrop': 'DoS',
# Probe attacks
'ipsweep': 'Probe', 'nmap': 'Probe', 'portsweep': 'Probe', 'satan': 'Probe',
# R2L attacks
'ftp_write': 'R2L', 'guess_passwd': 'R2L', 'imap': 'R2L',
'multihop': 'R2L', 'phf': 'R2L', 'spy': 'R2L', 'warezclient': 'R2L',
'warezmaster': 'R2L',
# U2R attacks
'buffer_overflow': 'U2R', 'loadmodule': 'U2R',
'perl': 'U2R', 'rootkit': 'U2R'
}
# Apply the mapping to the label column
attack_categories = y_train.map(attack_category_map)
# Count samples per category
category_counts = attack_categories.value_counts()
# Plot the distribution
plt.figure(figsize=(7, 5))
category_counts.plot(kind='bar')
plt.title("Attack Category Distribution in NSL-KDD Training Dataset")
plt.xlabel("Attack Category")
plt.ylabel("Number of Samples")
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
In [39]:
# Binary class bar chart (Normal vs Attack)
# Note to self: I’m collapsing all attack labels into one "attack" class
# to clearly show binary imbalance (normal vs attack).
import matplotlib.pyplot as plt
y_series = y_train
binary_labels = y_series.apply(
lambda x: "normal" if x == "normal" else "attack"
)
binary_counts = binary_labels.value_counts()
plt.figure(figsize=(8, 5)) # small enough to fit in one screenshot
plt.bar(binary_counts.index, binary_counts.values)
plt.title("Binary Class Distribution (Normal vs Attack)")
plt.xlabel("Traffic Type")
plt.ylabel("Number of Samples")
plt.tight_layout()
plt.show()
In [42]:
import matplotlib.pyplot as plt
# Extract the label column from the training dataset
y_series = train_df["label"]
# Map each fine-grained attack label to a high-level attack category
category_map = {
"normal": "Normal",
# DoS attacks
"back": "DoS", "land": "DoS", "neptune": "DoS",
"pod": "DoS", "smurf": "DoS", "teardrop": "DoS",
# Probe attacks
"ipsweep": "Probe", "nmap": "Probe",
"portsweep": "Probe", "satan": "Probe",
# R2L attacks
"ftp_write": "R2L", "guess_passwd": "R2L", "imap": "R2L",
"multihop": "R2L", "phf": "R2L",
"spy": "R2L", "warezclient": "R2L", "warezmaster": "R2L",
# U2R attacks
"buffer_overflow": "U2R", "loadmodule": "U2R",
"perl": "U2R", "rootkit": "U2R"
}
# Convert labels into attack categories
attack_categories = y_series.map(category_map).fillna("Other")
category_counts = attack_categories.value_counts()
# Ensure consistent category order for the plot
ordered_categories = ["Normal", "DoS", "Probe", "R2L", "U2R"]
category_counts = category_counts.reindex(ordered_categories).fillna(0)
# Plot the attack category distribution
plt.figure(figsize=(8, 5))
plt.bar(category_counts.index, category_counts.values)
plt.title("Attack Category Distribution in NSL-KDD Training Dataset")
plt.xlabel("Attack Category")
plt.ylabel("Number of Samples")
plt.tight_layout()
plt.show()
In [43]:
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
# Convert true labels to binary (normal vs attack)
y_test_binary = y_test.apply(lambda x: "normal" if x == "normal" else "attack")
# Convert predictions to binary
y_pred_rf_binary = pd.Series(y_pred_rf).apply(
lambda x: "normal" if x == "normal" else "attack"
)
# Compute confusion matrix
cm_binary = confusion_matrix(
y_test_binary,
y_pred_rf_binary,
labels=["normal", "attack"]
)
# Plot confusion matrix
plt.figure(figsize=(6, 5))
sns.heatmap(
cm_binary,
annot=True,
fmt="d",
cmap="Blues",
xticklabels=["normal", "attack"],
yticklabels=["normal", "attack"]
)
plt.xlabel("Predicted label")
plt.ylabel("True label")
plt.title("Random Forest – Binary Confusion Matrix (Normal vs Attack)")
plt.tight_layout()
plt.show()
In [44]:
from sklearn.metrics import classification_report
# Convert multi-class labels into binary labels (normal vs attack)
y_test_binary = y_test.apply(lambda x: "normal" if x == "normal" else "attack")
# Predict using the Random Forest pipeline
y_pred_rf = rf_pipeline.predict(X_test)
# Convert predictions into binary labels (normal vs attack)
y_pred_rf_binary = ["normal" if x == "normal" else "attack" for x in y_pred_rf]
# Print the binary classification report
print("Random Forest - Binary Classification Report (Test Set)")
print(classification_report(y_test_binary, y_pred_rf_binary))
Random Forest - Binary Classification Report (Test Set)
precision recall f1-score support
attack 1.00 1.00 1.00 11726
normal 1.00 1.00 1.00 13469
accuracy 1.00 25195
macro avg 1.00 1.00 1.00 25195
weighted avg 1.00 1.00 1.00 25195
In [45]:
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
# Convert true labels to binary (normal vs attack)
y_test_binary = y_test.apply(lambda x: "normal" if x == "normal" else "attack")
# Convert Gradient Boosting predictions to binary
y_pred_gb_binary = pd.Series(y_pred_gb).apply(
lambda x: "normal" if x == "normal" else "attack"
)
# Compute binary confusion matrix
cm_gb_binary = confusion_matrix(
y_test_binary,
y_pred_gb_binary,
labels=["normal", "attack"]
)
# Plot confusion matrix
plt.figure(figsize=(6, 5))
sns.heatmap(
cm_gb_binary,
annot=True,
fmt="d",
cmap="Blues",
xticklabels=["normal", "attack"],
yticklabels=["normal", "attack"]
)
plt.title("Gradient Boosting – Binary Confusion Matrix (Normal vs Attack)")
plt.xlabel("Predicted label")
plt.ylabel("True label")
plt.tight_layout()
plt.show()
In [46]:
from sklearn.metrics import classification_report
# Generate binary classification report for Gradient Boosting
gb_binary_report = classification_report(
y_test_binary,
y_pred_gb_binary,
target_names=["normal", "attack"]
)
print("Gradient Boosting - Binary Classification Report (Test Set)")
print(gb_binary_report)
Gradient Boosting - Binary Classification Report (Test Set)
precision recall f1-score support
normal 1.00 1.00 1.00 11726
attack 1.00 1.00 1.00 13469
accuracy 1.00 25195
macro avg 1.00 1.00 1.00 25195
weighted avg 1.00 1.00 1.00 25195
In [27]:
# Build + train the Gradient Boosting pipeline (so gb_pipeline exists for later figures)
from sklearn.pipeline import Pipeline
from sklearn.ensemble import GradientBoostingClassifier
# Quick check: I must already have X_train and y_train, and my preprocessor must exist
print("Do I have X_train?", "X_train" in globals())
print("Do I have y_train?", "y_train" in globals())
print("Do I have preprocessor?", "preprocessor" in globals())
# Create the Gradient Boosting pipeline using the SAME preprocessing as before
gb_pipeline = Pipeline(steps=[
("preprocessing", preprocessor),
("model", GradientBoostingClassifier(random_state=42))
])
# Train it (this is the missing step that puts it into memory)
gb_pipeline.fit(X_train, y_train)
print("✅ Trained Gradient Boosting pipeline saved as: gb_pipeline")
print("Pipeline steps:", gb_pipeline.named_steps.keys())
Do I have X_train? True Do I have y_train? True Do I have preprocessor? True ✅ Trained Gradient Boosting pipeline saved as: gb_pipeline Pipeline steps: dict_keys(['preprocessing', 'model'])
In [28]:
# Gradient Boosting Feature Importance (Top 15)
# This shows which input features the Gradient Boosting model relies on most.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Grab the trained Gradient Boosting model out of the pipeline
gb_model = gb_pipeline.named_steps["model"]
# Get the transformed feature names from the preprocessor
# (this matches exactly what the model actually trained on)
feature_names = gb_pipeline.named_steps["preprocessing"].get_feature_names_out()
# GradientBoostingClassifier stores importances here
importances = gb_model.feature_importances_
# Put feature names + importance into a tidy table, then sort biggest first
fi_df = pd.DataFrame({
"feature": feature_names,
"importance": importances
}).sort_values("importance", ascending=False)
# Pick the top 15 to keep the plot readable for a single screenshot
top_n = 15
top_features = fi_df.head(top_n)
# Plot as a horizontal bar chart (best for long feature names)
plt.figure(figsize=(8, 5))
plt.barh(top_features["feature"], top_features["importance"])
plt.gca().invert_yaxis()
plt.title("Gradient Boosting Feature Importance (Top 15)")
plt.xlabel("Importance Score")
plt.tight_layout()
plt.show()
# Print the top 15 table as well (useful for writing up exact feature names)
top_features
Out[28]:
| feature | importance | |
|---|---|---|
| 85 | num__src_bytes | 0.353045 |
| 109 | num__same_srv_rate | 0.297432 |
| 94 | num__root_shell | 0.069808 |
| 110 | num__diff_srv_rate | 0.065423 |
| 18 | cat__service_ecr_i | 0.032039 |
| 117 | num__dst_host_srv_diff_host_rate | 0.028940 |
| 98 | num__num_shells | 0.018702 |
| 118 | num__dst_host_serror_rate | 0.017449 |
| 27 | cat__service_http | 0.016603 |
| 77 | cat__flag_RSTR | 0.012098 |
| 86 | num__dst_bytes | 0.010561 |
| 90 | num__hot | 0.008186 |
| 102 | num__is_guest_login | 0.007643 |
| 114 | num__dst_host_same_srv_rate | 0.005914 |
| 97 | num__num_file_creations | 0.004327 |
In [31]:
# ROC Curve Calculation
# I'm explicitly checking which class the model considers "positive"
# so that ROC and AUC are calculated correctly.
from sklearn.metrics import roc_curve, auc
import matplotlib.pyplot as plt
import numpy as np
# Inspect class order used by each model
rf_classes = rf_pipeline.named_steps["model"].classes_
gb_classes = gb_pipeline.named_steps["model"].classes_
print("Random Forest classes:", rf_classes)
print("Gradient Boosting classes:", gb_classes)
# Identify the index corresponding to ATTACK traffic
# Anything that is NOT 'normal' is treated as an attack
attack_index_rf = np.where(rf_classes != "normal")[0][0]
attack_index_gb = np.where(gb_classes != "normal")[0][0]
# Get correct attack probabilities
rf_probs = rf_pipeline.predict_proba(X_test)[:, attack_index_rf]
gb_probs = gb_pipeline.predict_proba(X_test)[:, attack_index_gb]
# : Create correct binary ground truth
y_test_binary = (y_test != "normal").astype(int)
# Compute ROC and AUC
rf_fpr, rf_tpr, _ = roc_curve(y_test_binary, rf_probs)
gb_fpr, gb_tpr, _ = roc_curve(y_test_binary, gb_probs)
rf_auc = auc(rf_fpr, rf_tpr)
gb_auc = auc(gb_fpr, gb_tpr)
print(f"Random Forest AUC (fixed): {rf_auc:.4f}")
print(f"Gradient Boosting AUC (fixed): {gb_auc:.4f}")
# Plot corrected ROC curves
plt.figure(figsize=(8, 6))
plt.plot(rf_fpr, rf_tpr, label=f"Random Forest (AUC = {rf_auc:.3f})")
plt.plot(gb_fpr, gb_tpr, label=f"Gradient Boosting (AUC = {gb_auc:.3f})")
plt.plot([0, 1], [0, 1], linestyle="--")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("ROC Curve Comparison ")
plt.legend()
plt.tight_layout()
plt.show()
Random Forest classes: ['back' 'buffer_overflow' 'ftp_write' 'guess_passwd' 'imap' 'ipsweep' 'land' 'loadmodule' 'multihop' 'neptune' 'nmap' 'normal' 'perl' 'phf' 'pod' 'portsweep' 'rootkit' 'satan' 'smurf' 'spy' 'teardrop' 'warezclient' 'warezmaster'] Gradient Boosting classes: ['back' 'buffer_overflow' 'ftp_write' 'guess_passwd' 'imap' 'ipsweep' 'land' 'loadmodule' 'multihop' 'neptune' 'nmap' 'normal' 'perl' 'phf' 'pod' 'portsweep' 'rootkit' 'satan' 'smurf' 'spy' 'teardrop' 'warezclient' 'warezmaster'] Random Forest AUC (fixed): 0.5293 Gradient Boosting AUC (fixed): 0.1661
In [ ]: