In [1]:
# NSL-KDD ensemble experiment
# Save as nsl_kdd_ensembles.py or run cell-by-cell in Jupyter.
import time
import warnings
warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import StratifiedKFold, RandomizedSearchCV
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report, confusion_matrix, accuracy_score
In [2]:
# Loading the dataset
train = pd.read_excel('kdd_train.xlsx')
test = pd.read_excel('kdd_test.xlsx')
print("Train shape:", train.shape, "Test shape:", test.shape)
Train shape: (125973, 42) Test shape: (22544, 42)
In [3]:
train.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 [4]:
test.head()
Out[4]:
| 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
In [5]:
# Let us see the info of the training data
train.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 125973 entries, 0 to 125972 Data columns (total 42 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 duration 125973 non-null int64 1 protocol_type 125973 non-null object 2 service 125973 non-null object 3 flag 125973 non-null object 4 src_bytes 125973 non-null int64 5 dst_bytes 125973 non-null int64 6 land 125973 non-null int64 7 wrong_fragment 125973 non-null int64 8 urgent 125973 non-null int64 9 hot 125973 non-null int64 10 num_failed_logins 125973 non-null int64 11 logged_in 125973 non-null int64 12 num_compromised 125973 non-null int64 13 root_shell 125973 non-null int64 14 su_attempted 125973 non-null int64 15 num_root 125973 non-null int64 16 num_file_creations 125973 non-null int64 17 num_shells 125973 non-null int64 18 num_access_files 125973 non-null int64 19 num_outbound_cmds 125973 non-null int64 20 is_host_login 125973 non-null int64 21 is_guest_login 125973 non-null int64 22 count 125973 non-null int64 23 srv_count 125973 non-null int64 24 serror_rate 125973 non-null float64 25 srv_serror_rate 125973 non-null float64 26 rerror_rate 125973 non-null float64 27 srv_rerror_rate 125973 non-null float64 28 same_srv_rate 125973 non-null float64 29 diff_srv_rate 125973 non-null float64 30 srv_diff_host_rate 125973 non-null float64 31 dst_host_count 125973 non-null int64 32 dst_host_srv_count 125973 non-null int64 33 dst_host_same_srv_rate 125973 non-null float64 34 dst_host_diff_srv_rate 125973 non-null float64 35 dst_host_same_src_port_rate 125973 non-null float64 36 dst_host_srv_diff_host_rate 125973 non-null float64 37 dst_host_serror_rate 125973 non-null float64 38 dst_host_srv_serror_rate 125973 non-null float64 39 dst_host_rerror_rate 125973 non-null float64 40 dst_host_srv_rerror_rate 125973 non-null float64 41 labels 125973 non-null object dtypes: float64(15), int64(23), object(4) memory usage: 40.4+ MB
In [6]:
# Let us see the info of the testing data
test.info()
<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
In [7]:
# Checking unique values in each column
train.nunique()
Out[7]:
duration 2981 protocol_type 3 service 70 flag 11 src_bytes 3341 dst_bytes 9326 land 2 wrong_fragment 3 urgent 4 hot 28 num_failed_logins 6 logged_in 2 num_compromised 88 root_shell 2 su_attempted 3 num_root 82 num_file_creations 35 num_shells 3 num_access_files 10 num_outbound_cmds 1 is_host_login 2 is_guest_login 2 count 512 srv_count 509 serror_rate 89 srv_serror_rate 86 rerror_rate 82 srv_rerror_rate 62 same_srv_rate 101 diff_srv_rate 95 srv_diff_host_rate 60 dst_host_count 256 dst_host_srv_count 256 dst_host_same_srv_rate 101 dst_host_diff_srv_rate 101 dst_host_same_src_port_rate 101 dst_host_srv_diff_host_rate 75 dst_host_serror_rate 101 dst_host_srv_serror_rate 100 dst_host_rerror_rate 101 dst_host_srv_rerror_rate 101 labels 23 dtype: int64
In [8]:
# Checking unique values in each column
test.nunique()
Out[8]:
duration 704 protocol_type 3 service 66 flag 11 src_bytes 1517 dst_bytes 3745 land 2 wrong_fragment 3 urgent 3 hot 18 num_failed_logins 6 logged_in 2 num_compromised 29 root_shell 2 su_attempted 3 num_root 27 num_file_creations 14 num_shells 3 num_access_files 7 num_outbound_cmds 1 is_host_login 2 is_guest_login 2 count 476 srv_count 428 serror_rate 85 srv_serror_rate 66 rerror_rate 85 srv_rerror_rate 80 same_srv_rate 97 diff_srv_rate 92 srv_diff_host_rate 73 dst_host_count 255 dst_host_srv_count 255 dst_host_same_srv_rate 101 dst_host_diff_srv_rate 101 dst_host_same_src_port_rate 101 dst_host_srv_diff_host_rate 60 dst_host_serror_rate 100 dst_host_srv_serror_rate 100 dst_host_rerror_rate 101 dst_host_srv_rerror_rate 101 labels 36 dtype: int64
In [9]:
# Numerical summary of the training data
train.describe()
Out[9]:
| duration | src_bytes | dst_bytes | land | wrong_fragment | urgent | hot | num_failed_logins | logged_in | num_compromised | ... | 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 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 125973.00000 | 1.259730e+05 | 1.259730e+05 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | ... | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 | 125973.000000 |
| mean | 287.14465 | 4.556674e+04 | 1.977911e+04 | 0.000198 | 0.022687 | 0.000111 | 0.204409 | 0.001222 | 0.395736 | 0.279250 | ... | 182.148945 | 115.653005 | 0.521242 | 0.082951 | 0.148379 | 0.032542 | 0.284452 | 0.278485 | 0.118832 | 0.120240 |
| std | 2604.51531 | 5.870331e+06 | 4.021269e+06 | 0.014086 | 0.253530 | 0.014366 | 2.149968 | 0.045239 | 0.489010 | 23.942042 | ... | 99.206213 | 110.702741 | 0.448949 | 0.188922 | 0.308997 | 0.112564 | 0.444784 | 0.445669 | 0.306557 | 0.319459 |
| min | 0.00000 | 0.000000e+00 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 0.00000 | 0.000000e+00 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 82.000000 | 10.000000 | 0.050000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 50% | 0.00000 | 4.400000e+01 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 255.000000 | 63.000000 | 0.510000 | 0.020000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 75% | 0.00000 | 2.760000e+02 | 5.160000e+02 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | ... | 255.000000 | 255.000000 | 1.000000 | 0.070000 | 0.060000 | 0.020000 | 1.000000 | 1.000000 | 0.000000 | 0.000000 |
| max | 42908.00000 | 1.379964e+09 | 1.309937e+09 | 1.000000 | 3.000000 | 3.000000 | 77.000000 | 5.000000 | 1.000000 | 7479.000000 | ... | 255.000000 | 255.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 |
8 rows × 38 columns
In [10]:
# Numerical summary of the testing data
test.describe()
Out[10]:
| duration | src_bytes | dst_bytes | land | wrong_fragment | urgent | hot | num_failed_logins | logged_in | num_compromised | ... | 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 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 22544.000000 | 2.254400e+04 | 2.254400e+04 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | ... | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 | 22544.000000 |
| mean | 258.858499 | 1.226873e+04 | 2.966787e+03 | 0.000089 | 0.016989 | 0.000177 | 0.155562 | 0.008783 | 0.406095 | 0.259359 | ... | 186.878549 | 123.129613 | 0.545571 | 0.084905 | 0.141117 | 0.028275 | 0.229375 | 0.225729 | 0.155008 | 0.153560 |
| std | 2184.355090 | 4.665783e+05 | 6.293205e+04 | 0.009419 | 0.217507 | 0.016313 | 1.699324 | 0.104942 | 0.491114 | 11.370701 | ... | 97.508604 | 111.577695 | 0.447079 | 0.199813 | 0.305538 | 0.105966 | 0.410665 | 0.412500 | 0.339255 | 0.350511 |
| min | 0.000000 | 0.000000e+00 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 1.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 0.000000 | 0.000000e+00 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 95.000000 | 12.000000 | 0.050000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 50% | 0.000000 | 4.500000e+01 | 0.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 255.000000 | 85.000000 | 0.670000 | 0.020000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 75% | 0.000000 | 2.750000e+02 | 5.950000e+02 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | ... | 255.000000 | 255.000000 | 1.000000 | 0.070000 | 0.050000 | 0.010000 | 0.100000 | 0.010000 | 0.010000 | 0.000000 |
| max | 54451.000000 | 6.282565e+07 | 5.153771e+06 | 1.000000 | 3.000000 | 2.000000 | 30.000000 | 5.000000 | 1.000000 | 796.000000 | ... | 255.000000 | 255.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 |
8 rows × 38 columns
In [11]:
# Row index info of the train data
train.index
Out[11]:
RangeIndex(start=0, stop=125973, step=1)
In [12]:
# Row index info of the test data
test.index
Out[12]:
RangeIndex(start=0, stop=22544, step=1)
In [13]:
# 2) Identify label column (common names: 'label','labels','attack','class')
possible_label_cols = [c for c in train.columns if c.lower() in ("label","labels","class","attack","difficulty_level")]
label_col = possible_label_cols[0] if possible_label_cols else train.columns[-1]
print("Using label column:", label_col)
Using label column: labels
In [14]:
# 3) Map attack names to 5 classes
dos = {'back','land','neptune','pod','smurf','teardrop','apache2','udpstorm','processtable','mailbomb','worm'}
probe = {'satan','ipsweep','nmap','portsweep','mscan','saint'}
r2l = {'ftp_write','guess_passwd','imap','multihop','phf','spy','warezclient','warezmaster','sendmail'}
u2r = {'buffer_overflow','loadmodule','perl','rootkit','sqlattack','xterm'}
def map_attack(a):
a = str(a).lower().strip()
if a in ('normal','normal.'): return 'normal'
if a in dos: return 'dos'
if a in probe: return 'probe'
if a in r2l: return 'r2l'
if a in u2r: return 'u2r'
if any(x in a for x in ['scan','sweep','nmap','ips']): return 'probe'
if any(x in a for x in ['back','nept','smurf','teardrop','pod']): return 'dos'
if any(x in a for x in ['overflow','root','loadmod','perl','sql']): return 'u2r'
if any(x in a for x in ['warez','guess','ftp','imap','phf','spy']): return 'r2l'
if 'dos' in a: return 'dos'
return 'other'
train['attack_category'] = train[label_col].apply(map_attack)
test['attack_category'] = test[label_col].apply(map_attack)
print("Mapped class counts (train):")
print(train['attack_category'].value_counts())
print("Mapped class counts (test):")
print(test['attack_category'].value_counts())
Mapped class counts (train): attack_category normal 67343 dos 45927 probe 11656 r2l 995 u2r 52 Name: count, dtype: int64 Mapped class counts (test): attack_category normal 11245 dos 8095 probe 2157 r2l 918 other 100 u2r 29 Name: count, dtype: int64
In [15]:
# 4) Select features
# Common categorical fields in NSL-KDD: protocol_type, service, flag
cat_cols = [c for c in train.columns if c.lower() in ('protocol_type','service','flag')]
num_cols = [c for c in train.columns if c not in cat_cols + [label_col,'attack_category']]
# Ensure numeric columns are numeric
for c in num_cols:
if train[c].dtype == object:
train[c] = pd.to_numeric(train[c], errors='coerce')
test[c] = pd.to_numeric(test[c], errors='coerce')
train[num_cols] = train[num_cols].fillna(0)
test[num_cols] = test[num_cols].fillna(0)
In [16]:
# 5) Preprocessor: scale numeric, one-hot encode categorical
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
preprocessor = ColumnTransformer(transformers=[
('num', StandardScaler(), num_cols),
('cat', OneHotEncoder(handle_unknown='ignore', sparse_output=False), cat_cols)
])
In [19]:
# 6) For computational reasons: optionally subsample the training set for tuning/training
# (set sample_n=None to use the full training set)
sample_n = 20000 # set to None or a larger number if you have more compute
if sample_n is not None and train.shape[0] > sample_n:
# stratified sample by attack_category
frac = sample_n / train.shape[0]
sample = train.groupby('attack_category', group_keys=False).apply(
lambda df: df.sample(frac=min(1, frac), random_state=42)
).sample(n=min(sample_n, train.shape[0]), random_state=42)
else:
sample = train.copy()
print("Using sample size:", sample.shape)
X_sample = sample.drop(columns=[label_col,'attack_category'])
y_sample = sample['attack_category']
X_test = test.drop(columns=[label_col,'attack_category'])
y_test = test['attack_category']
Using sample size: (20000, 43)
In [20]:
# 7) Define models and pipelines
rf = RandomForestClassifier(random_state=42, n_jobs=1)
gb = GradientBoostingClassifier(random_state=42, n_estimators=100, learning_rate=0.1, max_depth=3)
pipe_rf = Pipeline([('pre', preprocessor), ('clf', rf)])
pipe_gb = Pipeline([('pre', preprocessor), ('clf', gb)])
In [21]:
# 8) Hyperparameter tuning for RF (small randomized search)
rf_param_dist = {
'clf__n_estimators': [50, 100, 150],
'clf__max_depth': [None, 10, 20],
'clf__min_samples_split': [2, 5, 10]
}
cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42)
rs_rf = RandomizedSearchCV(pipe_rf, rf_param_dist, n_iter=6, cv=cv, scoring='f1_macro', n_jobs=1, random_state=42, verbose=1)
t0 = time.time()
rs_rf.fit(X_sample, y_sample)
rf_search_time = time.time() - t0
print("RF best params:", rs_rf.best_params_, "search time:", rf_search_time)
Fitting 3 folds for each of 6 candidates, totalling 18 fits
RF best params: {'clf__n_estimators': 50, 'clf__min_samples_split': 2, 'clf__max_depth': None} search time: 8.22621750831604
In [22]:
# 9) For GB we will use the default/sensible param values (or you can run a small search)
# (If you have the compute, create a similar RandomizedSearchCV for GB.)
# Fit the chosen/found models on the sampled training set
best_rf = rs_rf.best_estimator_
print("Fitting best RF on the sample...")
t0 = time.time()
best_rf.fit(X_sample, y_sample)
rf_fit_time = time.time() - t0
print("Fitting default GB on the sample...")
t0 = time.time()
pipe_gb.fit(X_sample, y_sample)
gb_fit_time = time.time() - t0
Fitting best RF on the sample... Fitting default GB on the sample...
In [23]:
# 10) Evaluate on test set
y_pred_rf = best_rf.predict(X_test)
y_pred_gb = pipe_gb.predict(X_test)
print("\nRandom Forest classification report:\n")
print(classification_report(y_test, y_pred_rf, digits=4))
print("\nGradient Boosting classification report:\n")
print(classification_report(y_test, y_pred_gb, digits=4))
print("\nAccuracy - RF: {:.4f}, GB: {:.4f}".format(accuracy_score(y_test, y_pred_rf), accuracy_score(y_test, y_pred_gb)))
Random Forest classification report:
precision recall f1-score support
dos 0.9895 0.9354 0.9617 8095
normal 0.8702 0.9927 0.9274 11245
other 0.0000 0.0000 0.0000 100
probe 0.9623 0.8642 0.9106 2157
r2l 0.9918 0.1318 0.2327 918
u2r 0.6000 0.1034 0.1765 29
accuracy 0.9192 22544
macro avg 0.7356 0.5046 0.5348 22544
weighted avg 0.9226 0.9192 0.9048 22544
Gradient Boosting classification report:
precision recall f1-score support
dos 0.9903 0.9576 0.9737 8095
normal 0.8944 0.9900 0.9398 11245
other 0.0000 0.0000 0.0000 100
probe 0.9387 0.8739 0.9052 2157
r2l 0.9447 0.2233 0.3612 918
u2r 0.2273 0.3448 0.2740 29
accuracy 0.9308 22544
macro avg 0.6659 0.5650 0.5756 22544
weighted avg 0.9303 0.9308 0.9201 22544
Accuracy - RF: 0.9192, GB: 0.9308
In [24]:
# 11) Confusion matrices
labels = ['normal','dos','probe','r2l','u2r','other']
cm_rf = confusion_matrix(y_test, y_pred_rf, labels=labels)
cm_gb = confusion_matrix(y_test, y_pred_gb, labels=labels)
fig, axs = plt.subplots(1,2, figsize=(14,5))
axs[0].imshow(cm_rf, interpolation='nearest')
axs[0].set_title('RF Confusion Matrix')
axs[0].set_xticks(range(len(labels))); axs[0].set_yticks(range(len(labels)))
axs[0].set_xticklabels(labels, rotation=45); axs[0].set_yticklabels(labels)
for i in range(cm_rf.shape[0]):
for j in range(cm_rf.shape[1]):
axs[0].text(j, i, cm_rf[i,j], ha='center', va='center', color='white' if cm_rf[i,j] > cm_rf.max()/2 else 'black')
axs[1].imshow(cm_gb, interpolation='nearest')
axs[1].set_title('GB Confusion Matrix')
axs[1].set_xticks(range(len(labels))); axs[1].set_yticks(range(len(labels)))
axs[1].set_xticklabels(labels, rotation=45); axs[1].set_yticklabels(labels)
for i in range(cm_gb.shape[0]):
for j in range(cm_gb.shape[1]):
axs[1].text(j, i, cm_gb[i,j], ha='center', va='center', color='white' if cm_gb[i,j] > cm_gb.max()/2 else 'black')
plt.tight_layout(); plt.show()
In [25]:
# 12) Comparison of Random Forest vs Gradient Boosting
import time
# Measure training time for Random Forest
start = time.time()
best_rf.fit(X_sample, y_sample)
rf_time = time.time() - start
# Measure training time for Gradient Boosting
start = time.time()
pipe_gb.fit(X_sample, y_sample)
gb_time = time.time() - start
print("Training Time (seconds):")
print(f"Random Forest: {rf_time:.2f} seconds")
print(f"Gradient Boosting: {gb_time:.2f} seconds")
Training Time (seconds): Random Forest: 0.40 seconds Gradient Boosting: 15.58 seconds
In [26]:
model_times = {"Random Forest": rf_time, "Gradient Boosting": gb_time}
model_times
Out[26]:
{'Random Forest': 0.4006197452545166, 'Gradient Boosting': 15.579497575759888}
In [27]:
# 13) Feature importances (RF and GB)
pre = best_rf.named_steps['pre']
num_features = num_cols
cat_encoder = pre.named_transformers_['cat']
try:
cat_features = list(cat_encoder.get_feature_names_out(cat_cols))
except Exception:
# fallback if older sklearn
cat_features = []
for col in cat_cols:
values = sorted(sample[col].astype(str).unique())
cat_features.extend([f"{col}__{v}" for v in values])
all_features = num_features + cat_features
rf_imps = best_rf.named_steps['clf'].feature_importances_
gb_imps = pipe_gb.named_steps['clf'].feature_importances_
imp_df = pd.DataFrame({"feature": all_features, "rf_importance": rf_imps, "gb_importance": gb_imps})
imp_df = imp_df.sort_values("rf_importance", ascending=False).reset_index(drop=True)
print("Top features by RF importance:")
print(imp_df.head(20))
Top features by RF importance:
feature rf_importance gb_importance
0 src_bytes 0.100335 0.387996
1 diff_srv_rate 0.066176 0.031542
2 flag_S0 0.059355 0.002485
3 dst_host_serror_rate 0.057830 0.093042
4 flag_SF 0.057699 0.000022
5 dst_host_diff_srv_rate 0.057490 0.051138
6 dst_bytes 0.049812 0.025881
7 same_srv_rate 0.047991 0.165326
8 dst_host_same_srv_rate 0.035741 0.012401
9 serror_rate 0.034991 0.022129
10 count 0.033569 0.020397
11 dst_host_srv_diff_host_rate 0.032294 0.000577
12 dst_host_srv_count 0.029690 0.001365
13 dst_host_same_src_port_rate 0.029662 0.007624
14 srv_serror_rate 0.028490 0.000102
15 logged_in 0.022835 0.001407
16 srv_count 0.021627 0.000044
17 dst_host_srv_rerror_rate 0.020713 0.000154
18 dst_host_count 0.020206 0.000454
19 dst_host_srv_serror_rate 0.019995 0.000144
In [28]:
# Plot top 15 features
top_n = 15
top_rf = imp_df.head(top_n).iloc[::-1] # reverse for plotting
top_gb = imp_df.sort_values("gb_importance", ascending=False).head(top_n).iloc[::-1]
fig, ax = plt.subplots(1,2, figsize=(16,7))
ax[0].barh(range(len(top_rf)), top_rf['rf_importance'])
ax[0].set_yticks(range(len(top_rf))); ax[0].set_yticklabels(top_rf['feature'])
ax[0].set_title("Top RF importances")
ax[1].barh(range(len(top_gb)), top_gb['gb_importance'])
ax[1].set_yticks(range(len(top_gb))); ax[1].set_yticklabels(top_gb['feature'])
ax[1].set_title("Top GB importances")
plt.tight_layout(); plt.show()
In [ ]: