In [1]:
# ================================================================
# I am still learing Python and I am still unsure what Im doing!!
#
# INTRUSION DETECTION USING MODELS ON NSL-KDD-LIKE DATSETA
# Dataset: kdd_test.csv (42 columns: 41 features + 'labels')
#
# Models: Random Forest & Gradient Boosting (multi-class classificatio Jupyter)
# ================================================================
# -----------------------------
# IMPORT PYTHON LIBRARIES
# ------------------i-----ing--
# We import all reqfor the assinmenteep things tidy.
import os
import numpy as np # For numerical operations
import pandas as pd # For data loading and manipulation
import time # For timing
import matplotlib.pyplot as plt # For plotting graphs
import seaborn as sns # For nicer visualisations
from sklearn.model_selection import train_test_split, RandomizedSearchCV
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
accuracy_score,
classification_report,
confusion_matrix
)
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from IPython.display import display
# contstentdom seed so results are reproducible#
RANDOM_STATE = 42
np.random.seed(RANDOM_STATE)
# Make plots a bit prettier
sns.set(style="whitegrid")
# --To -------------------------------------
# LOAD THE DATASET (kdd_test.csv)
# T-------------------------------
# We assume the file "kdd_test.cshis notebook.
# If not, updatsv" It # <--- change this if your file is elsewhere
data_path = "C:\\Users\\paul_\\OneDrive\\Desktop\\Artificial Intelligence\\kdd_test.csv"
print("Exists:", os.path.exists(data_path))
print("Is file:", os.path.isfile(data_path))
print("Readable:", os.access(data_path, os.R_OK))
# Read the CSV file intoaFrame
df = pd.read_csv(data_path)
# Take a quick look at the first few rows to understand the structure
print("Fi'srthe st 5 rows of the dataset:")
display(df.head())
# Show shape (rows, columns)
print("\nDataset shape (rows, columns):", df.shape)
# ---------------------------------------
# BASIC DATA INSPECTIOEUNDERedSTANDING
# ---------------------------------------
# We expect:
# - 41 feature columns (numeric + categorical)
# - 1 target column (called 'labels' in this file)
print("\nColumn names:")
print(df.columns.tolist())
print("\nInfo This about's the dataset (data tyHopfully theren-null counts):")
print(df.info())
# Check for missing values (ideally should be none)
# pr r of missing values per column:")
#print(din the 'labels' column.
# Let's see what classes we have and how many of each.
print("\nClass diAn stributin (value counts for 'labels'):")
print(df['labels'].value_counts())
# Optional: visualise the class distribution
plt.figure(figsize=(10, 4))
df['labels'].value_counts().plot(kind='bar')
plt.title("Class Distribution (All Data)")
plt.xlabel("Class (Attack type / normal)")
plt.ylabel("Count")
plt.tight_layout()
plt.show()
# ---------------------------------------
# TRAIN-TEST SPitT
# ---------------------------------------
# Even though this file is nast.csv, we wilure theas a full dataset
# and split it into tra and test sets ourselves. This makes sure we have
# data for training AND separate data for fair evaluation.
# Features (X) = all colThis part umns ex'scept 'labels'
# Target (y) = 'labels'
X = df.drop(columns=['labels'])
y = df['labels']
# Perform an 80/20 train-test split, stratified by the class labels
# so each split keeps approximately the same class proportion.
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
stratify=y,
random_state=RANDOM_STATE
)
print("\nTrain set shape:", X_train.shape, "Test set shape:", X_test.shape)
# ---------------------the ------------------
# IDENTIFY CATEGORICAL AND NUMERIC FEATURES
# ---------------------------------------
# From NSL-KDD documentation, the mToricis needed tos are: the following feature
# - protocol_type
# - service
# - flag
#
# The rest of the input features are numeric.
# - need this so it can apply:
# - OneHotEncoder to categorical features
# - StandardScaler to numeric features
categorical_cols = ['protocol_type', 'service', 'flag']
numeric_cols = [col for col in X_train.columns if col not in categorical_cols]
print("\nCategorical columns:", categorical_cols)
print("Number of numeric columns:", len(numeric_cols))
#Using--------------------------------------
# BUILD PREPROCESSOR (ENCODING + SCALING)
# ---------------------------------------
# use ColumnTransformer to apply different transformations to different columns:
# - OneHotEncoder: converts each category in a thegorical feature into a binary column.
# - StandardScaler: standardises numeric features to have mean 0 and variance 1.
#
# This helps many ML models perform better and converge faster.
categorical_transformer = OneHotEncoder(handle_unknown='ignore')
numeric_transformer = StandardScaler()
preprocessor = ColumnTransformer(
transformers=[
('cat', categorical_transformer, categorical_cols),
('num', numeric_transformer, numeric_cols)
]
)
# ---------------------------------------
# DEFINE THE ENSEMBLE MODELS
# ---------------------------------------
# look though
# - RandomForestClassifier (bagging ensemble)
# - GradientBoostingClassifier (boosting ensemble)
#
# Both will be wrapped in Pipelines that first preprocess the data and then fit the model.
rf_clf = RandomForestClassifier(
random_state=RANDOM_STATE,
n_jobs=-1 # Use all CPU cores for speed
)
gb_clf = GradientBoostingClassifier(
random_state=RANDOM_STATE
)
rf_pipeline = Pipeline(steps=[
('preprocess', preprocessor),
('model', rf_clf)
])
gb_pipeline = Pipeline(steps=[
('preprocess', preprocessor),
('model', gb_clf)
])
# -will be used --------------------------------------
# Hit is RPARAMdEtheER TUNING (RandomizedSearchCV)
# ---------------------------------------
# do not want to use the default hyperparameters only, as they might be suboptimal.
# Instead, we define a seaform cross-validation
# - find a reasonably good set of parameters without taking too long.
#
# NOTE: reduce n_iter if the machine is slow, or increase if you want better tuning?.
# Hyperparameter distributions for Random Forest
rf_param_dist = {
'model__n_estimators': [100, 200, 300], # Number of trees
'model__max_depth': [None, 10, 20, 30], # Depth of each tree
'model__min_samples_split': [2, 5, 10], # Min samples to split an internal node
'model__min_samples_leaf': [1, 2, 4], # Min samples at each leaf node
'model__max_features': ['sqrt', 'log2'] # Number of features considered at each split
}
# Hyperparameter distributions for Gradient Boosting
gb_param_dist = {
'model__n_estimators': [100, 200], # Number of boosting stages
'model__learning_rate': [0.01, 0.05, 0.1], # Shrinks contribution of each tree
'model__max_depth': [3, 5], # Depth of individual regression estimators
'model__subsample': [0.6, 0.8, 1.0] # Fraction of samples used for fitting each base learner
}
# Randomised search for RF
rf_search = RandomizedSearchCV(
estimator=rf_pipeline,
param_distributions=rf_param_dist,
n_iter=15, # Number of random combinations to try
scoring='f1_macro', # Macro F1 because we care about all classeradient soosting equally
cv=3, # 3-fold cross-validation
verbose=1, # Print progress
n_jobs=-1, # Use all cores
random_state=RANDOM_STATE
)
# Randomised search for GB
gb_search = RandomizedSearchCV(
estimator=gb_pipeline,
param_distributions=gb_param_dist,
n_iter=15,
scoring='f1_macro',
cv=3,
verbose=1,
n_jobs=-1,
random_state=RANDOM_STATE
)
# ---------------------------------------
# TRAIN (TUNE) RANDOM FOREST
# ---------------------------------------
print("\n========================")
print(" TUNING RANDOM FOREST ")
print("========================")
start_time = time.perf_counter()
rf_search.fit(X_train, y_train)
rf_time = time.perf_counter() - start_time
print("\nBest RF hyperparameters found:")
print(rf_search.best_params_)
print("Best RF cross-validated F1-macro:", rf_search.best_score_)
print("RF training + tuning time: {:.2f} seconds".format(rf_time))
# ---------------------------------------
# TRAIN (TUNE) GRADIENT BOOSTING
# ---------------------------------------
print("\n===========================")
print(" TUNING GRADIENT BOOSTING ")
print("===========================")
start_time = time.perf_counter()
gb_search.fit(X_train, y_train)
gb_time = time.perf_counter() - start_time
print("\nBest GB hyperparameters found:")
print(gb_search.best_params_)
print("BIf all goes well this will :s-validated F1-macro:", gb_search.best_score_)
print("GB training + tuning time: {:.2f} seconds".format(gb_time))
# ---------------------------------------
# EVALUATE BOTH MODELS ON TRAIN AND TEST SETS
# ---------------------------------------
# I hope it:
# - Measure accuracy, precision, recall, F1
# - Inspect confusion matrices
# - Compare train vs test performance (overfitting / underfitting)
# - Compare training times (computational efficiency)
# Get the best estimators (pipelines) from the searches
best_rf = rf_search.best_estimator_
best_gb = gb_search.best_estimator_
# Predictions on TRAIN set
rf_train_pred = best_rf.predict(X_train)
gb_train_pred = best_gb.predict(X_train)
# Predictions on TEST set
rf_test_pred = best_rf.predict(X_test)
gb_test_pred = best_gb.predict(X_test)
# Help to print metrics for a model
def print_model_performance(name, y_train, y_train_pred, y_test, y_test_pred):
print(f"\n========== {name} PERFORMANCE ==========")
# Accuracy
train_acc = accuracy_score(y_train, y_train_pred)
test_acc = accuracy_score(y_test, y_test_pred)
print("Train accuracy:", train_acc)
print("Test accuracy:", test_acc)
# Full classification report on test set (precision, recall, F1 per class)
print("\nClassification report (Test):")
print(classification_report(y_test, y_test_pred))
# Return accuracy for potential further comparison
return train_acc, test_acc
rf_train_acc, rf_test_acc = print_model_performance(
"Random Forest",
y_train, rf_train_pred,
y_test, rf_test_pred
)
gb_train_acc, gb_test_acc = print_model_performance(
"Gradient Boosting",
y_train, gb_train_pred,
y_test, gb_test_pred
)
# ---------------------------------------
# CONFUSION MATRICES (VISUAL)
# ---------------------------------------
# Confusion matrices show how often each true class is predicted
# as each other class. Very useful for diagnosis of which classes
# are being confused.
labels_sorted = sorted(y.unique()) # Sort class labels for consistent ordering
rf_cm = confusion_matrix(y_test, rf_test_pred, labels=labels_sorted)
gb_cm = confusion_matrix(y_test, gb_test_pred, labels=labels_sorted)
def plot_confusion_matrix(cm, classes, title):
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=False, fmt="d", cmap="Blues",
xticklabels=classes, yticklabels=classes)
plt.xlabel("Predicted label")
plt.ylabel("True label")
plt.title(title)
plt.tight_layout()
plt.show()
print("Random Forest: {:.2f} seconds".format(rf_time))
print("Gradient Boost: {:.2f} seconds".format(gb_time))
plot_confusion_matrix(rf_cm, labels_sorted, "Random Forest - Confusion Matrix (Test Set)")
plot_confusion_matrix(gb_cm, labels_sorted, "Gradient Boosting - Confusion Matrix (Test Set)")
# At this point it should do the following:
# - which model performs better (accuracy / F1)
# - overfitting: if train_acc >> test_acc, model may be overfitting
# - Compare RF vs GB in terms of training time (efficiency)
# ---------------------------------------
# FEATURE IMPORTANCE & INTERPRETATION
# ---------------------------------------
# Ensemble tree-based models (RF and GB) have a built-in notion of feature importance:
# - How much each feature reduces impurity across trees.
# want to:
# - Extract feature importances
# - Map them back to human-readable feature names (after OneHotEncoding)
# - Plot the top features
# - Use them for insights/recommendations in the report.
# First, the fitted preprocessor and model from the pipeline.
rf_preprocessor = best_rf.named_steps['preprocess']
rf_model = best_rf.named_steps['model']
gb_preprocessor = best_gb.named_steps['preprocess']
gb_model = best_gb.named_steps['model']
# Obtain the feature names:
# - For the categorical part: use OneHotEncoder's get_feature_names_out
# - For the numeric part: it's just the original numeric column names
ohe = rf_preprocessor.named_transformers_['cat']
ohe_feature_names = list(ohe.get_feature_names_out(categorical_cols))
all_feature_names = ohe_feature_names + numeric_cols
rf_importances = pd.DataFrame({
'feature': all_feature_names,
'importance': rf_model.feature_importances_
}).sort_values(by='importance', ascending=False)
gb_importances = pd.DataFrame({
'feature': all_feature_names,
'importance': gb_model.feature_importances_
}).sort_values(by='importance', ascending=False)
print("\nTop 15 Random Forest features by importance:")
display(rf_importances.head(15))
# Plot top N most important features for RF
top_n = 15
plt.figure(figsize=(8, 6))
sns.barplot(data=rf_importances.head(top_n),
x='importance', y='feature')
plt.title(f"Random Forest - Top {top_n} Feature Importances")
plt.tight_layout()
plt.show()
print("\nTop 15 Gradient Boostinmportance:")
display(gb_importances.head(15))
plt.figure(figsize=(8, 6))
sns.barplot(data=gb_importances.head(top_n),
x='importance', y='feature')
plt.title(f"Gradient Boosting - Top {top_n} Feature Importances")
plt.tight_layout()
plt.show()
Exists: True Is file: True Readable: True Fi'srthe st 5 rows of the dataset:
| 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 | 5 | tcp | smtp | SF | 2429 | 475 | 0 | 0 | 0 | 0 | ... | 57 | 0.11 | 0.02 | 0.01 | 0.04 | 0.0 | 0.0 | 0.87 | 0.0 | normal |
| 1 | 0 | udp | domain_u | SF | 45 | 134 | 0 | 0 | 0 | 0 | ... | 155 | 0.80 | 0.02 | 0.01 | 0.00 | 0.0 | 0.0 | 0.00 | 0.0 | normal |
| 2 | 0 | udp | domain_u | SF | 45 | 80 | 0 | 0 | 0 | 0 | ... | 255 | 1.00 | 0.00 | 0.01 | 0.00 | 0.0 | 0.0 | 0.00 | 0.0 | normal |
| 3 | 1979 | udp | other | SF | 145 | 105 | 0 | 0 | 0 | 0 | ... | 2 | 0.01 | 0.84 | 1.00 | 0.00 | 0.0 | 0.0 | 0.00 | 0.0 | normal |
| 4 | 14462 | tcp | other | RSTR | 1 | 0 | 0 | 0 | 0 | 0 | ... | 2 | 0.01 | 0.68 | 1.00 | 0.00 | 0.0 | 0.0 | 1.00 | 1.0 | portsweep |
5 rows × 42 columns
Dataset shape (rows, columns): (22544, 42) Column names: ['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', 'labels'] Info This about's the dataset (data tyHopfully theren-null counts): <class 'pandas.core.frame.DataFrame'> RangeIndex: 22544 entries, 0 to 22543 Data columns (total 42 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 duration 22544 non-null int64 1 protocol_type 22544 non-null object 2 service 22544 non-null object 3 flag 22544 non-null object 4 src_bytes 22544 non-null int64 5 dst_bytes 22544 non-null int64 6 land 22544 non-null int64 7 wrong_fragment 22544 non-null int64 8 urgent 22544 non-null int64 9 hot 22544 non-null int64 10 num_failed_logins 22544 non-null int64 11 logged_in 22544 non-null int64 12 num_compromised 22544 non-null int64 13 root_shell 22544 non-null int64 14 su_attempted 22544 non-null int64 15 num_root 22544 non-null int64 16 num_file_creations 22544 non-null int64 17 num_shells 22544 non-null int64 18 num_access_files 22544 non-null int64 19 num_outbound_cmds 22544 non-null int64 20 is_host_login 22544 non-null int64 21 is_guest_login 22544 non-null int64 22 count 22544 non-null int64 23 srv_count 22544 non-null int64 24 serror_rate 22544 non-null float64 25 srv_serror_rate 22544 non-null float64 26 rerror_rate 22544 non-null float64 27 srv_rerror_rate 22544 non-null float64 28 same_srv_rate 22544 non-null float64 29 diff_srv_rate 22544 non-null float64 30 srv_diff_host_rate 22544 non-null float64 31 dst_host_count 22544 non-null int64 32 dst_host_srv_count 22544 non-null int64 33 dst_host_same_srv_rate 22544 non-null float64 34 dst_host_diff_srv_rate 22544 non-null float64 35 dst_host_same_src_port_rate 22544 non-null float64 36 dst_host_srv_diff_host_rate 22544 non-null float64 37 dst_host_serror_rate 22544 non-null float64 38 dst_host_srv_serror_rate 22544 non-null float64 39 dst_host_rerror_rate 22544 non-null float64 40 dst_host_srv_rerror_rate 22544 non-null float64 41 labels 22544 non-null object dtypes: float64(15), int64(23), object(4) memory usage: 7.2+ MB None Class diAn stributin (value counts for 'labels'): labels normal 11245 neptune 6654 satan 698 smurf 540 ipsweep 479 guess_passwd 411 portsweep 371 mscan 310 warezmaster 279 apache2 228 back 227 processtable 211 nmap 206 warezclient 107 teardrop 104 snmpguess 99 mailbomb 94 saint 93 snmpgetattack 43 httptunnel 41 pod 35 buffer_overflow 17 multihop 9 ps 9 sendmail 8 xlock 4 loadmodule 4 rootkit 4 xterm 3 land 2 imap 2 ftp_write 2 named 2 phf 1 perl 1 xsnoop 1 Name: count, dtype: int64
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[1], line 104 100 y = df['labels'] 102 # Perform an 80/20 train-test split, stratified by the class labels 103 # so each split keeps approximately the same class proportion. --> 104 X_train, X_test, y_train, y_test = train_test_split( 105 X, y, 106 test_size=0.2, 107 stratify=y, 108 random_state=RANDOM_STATE 109 ) 111 print("\nTrain set shape:", X_train.shape, "Test set shape:", X_test.shape) 113 # ---------------------the ------------------ 114 # IDENTIFY CATEGORICAL AND NUMERIC FEATURES 115 # --------------------------------------- (...) 123 # - OneHotEncoder to categorical features 124 # - StandardScaler to numeric features File ~\anaconda3\Lib\site-packages\sklearn\utils\_param_validation.py:213, in validate_params.<locals>.decorator.<locals>.wrapper(*args, **kwargs) 207 try: 208 with config_context( 209 skip_parameter_validation=( 210 prefer_skip_nested_validation or global_skip_validation 211 ) 212 ): --> 213 return func(*args, **kwargs) 214 except InvalidParameterError as e: 215 # When the function is just a wrapper around an estimator, we allow 216 # the function to delegate validation to the estimator, but we replace 217 # the name of the estimator by the name of the function in the error 218 # message to avoid confusion. 219 msg = re.sub( 220 r"parameter of \w+ must be", 221 f"parameter of {func.__qualname__} must be", 222 str(e), 223 ) File ~\anaconda3\Lib\site-packages\sklearn\model_selection\_split.py:2801, in train_test_split(test_size, train_size, random_state, shuffle, stratify, *arrays) 2797 CVClass = ShuffleSplit 2799 cv = CVClass(test_size=n_test, train_size=n_train, random_state=random_state) -> 2801 train, test = next(cv.split(X=arrays[0], y=stratify)) 2803 train, test = ensure_common_namespace_device(arrays[0], train, test) 2805 return list( 2806 chain.from_iterable( 2807 (_safe_indexing(a, train), _safe_indexing(a, test)) for a in arrays 2808 ) 2809 ) File ~\anaconda3\Lib\site-packages\sklearn\model_selection\_split.py:1843, in BaseShuffleSplit.split(self, X, y, groups) 1813 """Generate indices to split data into training and test set. 1814 1815 Parameters (...) 1840 to an integer. 1841 """ 1842 X, y, groups = indexable(X, y, groups) -> 1843 for train, test in self._iter_indices(X, y, groups): 1844 yield train, test File ~\anaconda3\Lib\site-packages\sklearn\model_selection\_split.py:2247, in StratifiedShuffleSplit._iter_indices(self, X, y, groups) 2245 class_counts = np.bincount(y_indices) 2246 if np.min(class_counts) < 2: -> 2247 raise ValueError( 2248 "The least populated class in y has only 1" 2249 " member, which is too few. The minimum" 2250 " number of groups for any class cannot" 2251 " be less than 2." 2252 ) 2254 if n_train < n_classes: 2255 raise ValueError( 2256 "The train_size = %d should be greater or " 2257 "equal to the number of classes = %d" % (n_train, n_classes) 2258 ) ValueError: The least populated class in y has only 1 member, which is too few. The minimum number of groups for any class cannot be less than 2.
In [ ]:
In [ ]: