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()
No description has been provided for this image
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()
No description has been provided for this image
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

No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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
No description has been provided for this image
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
No description has been provided for this image
In [ ]: