Network Intrusion Detection + AI

Detect network attacks with ML and explain alerts in plain English using Claude.

Introduction

30 Second Summary

Security tools flag thousands of suspicious network flows every day, but the raw output is a wall of numbers that only a machine learning expert can interpret. Junior analysts need to know why something was flagged and what to do about it.

In this project, you will build a cybersecurity analysis pipeline that combines a Random Forest classifier with Claude to detect network intrusions and explain them in plain English. You will feed in real network traffic, flag the malicious flows, and produce analyst-ready threat reports.

What You'll Build

You run a single script, it classifies hundreds of thousands of network flows, and for every malicious detection it prints a plain-English explanation like "This flow was flagged as DoS Slowloris because it has an abnormally long duration with tiny packet sizes, consistent with an attacker holding connections open to exhaust server resources."

By the end of this project, you'll have:

  • A trained intrusion detection model that catches five different attack types across real network traffic with ~97%+ accuracy.
  • An LLM explainability layer where Claude translates cryptic ML predictions into 3-4 sentence analyst reports naming the exact features that triggered the alert and recommending a response action.
  • A complete pipeline script you run from the command line that outputs a formatted threat report with summary statistics and individual alert cards for every malicious flow.
  • Secret Mission: Integrate SHAP values for per-prediction feature attribution, making Claude's explanations mathematically grounded with exact contribution scores.

Are there any prerequisites?

No prior machine learning or cybersecurity experience is needed. You will need Python installed, a code editor, and an Anthropic API key. This guide walks you through setting up everything else.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Environment and Dataset

Before you can train a model or generate explanations, you need the right tools installed and the dataset ready.

In this step, you will create a project folder, set up a Python virtual environment with the machine learning and API libraries, and download the network traffic dataset your classifier will learn from.

In this step, get ready to:
  • Create a project folder and virtual environment with all dependencies installed.
  • Download the CICIDS2017 Wednesday dataset from Kaggle.
  • Set your Anthropic API key as an environment variable and verify the setup.
Create your project folder and install dependencies

Your project needs three Python packages: scikit-learn for the Random Forest classifier, pandas for data manipulation, and anthropic for calling the Claude API.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir nids-project
cd nids-project
  • Confirm the folder exists by running:
pwd

You should see a path ending in nids-project (something like /Users/yourname/Desktop/nids-project).

  • Create a virtual environment by running this command:

macOS

python3 -m venv venv
  • Activate the virtual environment by running:
source venv/bin/activate

Windows

python -m venv venv
  • Activate the virtual environment by running:
venv\Scripts\activate

You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

What is a virtual environment?

A virtual environment is an isolated Python installation that keeps this project's packages separate from your system Python. It prevents version conflicts between different projects on your machine.

  • Install the three project dependencies by running:
pip install scikit-learn pandas anthropic

You will see download progress bars and installation messages for each package. The install takes about 30-60 seconds depending on your internet speed.

Seeing a permissions error?

Make sure you see (venv) in your terminal prompt. If you do not, the virtual environment is not active. Run the activation command again from above.

Help me fix this permissions error

Download the CICIDS2017 dataset

The CICIDS2017 dataset is a labeled network traffic dataset from the Canadian Institute for Cybersecurity. The Wednesday file contains five attack types your model will learn to detect: DoS Hulk, DoS GoldenEye, Slowloris, Slowhttptest, and Heartbleed.

  • Open your browser and navigate to the Kaggle dataset page:
  • https://www.kaggle.com/datasets/chethuhn/network-intrusion-dataset
  • Sign in to Kaggle (or create a free account if you do not have one).
  • Click the Download button near the top of the dataset page to download the ZIP file.
  • Extract the ZIP file after it downloads.
  • Find the file named Wednesday-workingHours.pcap_ISCX.csv inside the extracted folder.
  • Move Wednesday-workingHours.pcap_ISCX.csv into your nids-project folder on your Desktop.
  • Confirm the file is in your project folder by running:
ls

You should see Wednesday-workingHours.pcap_ISCX.csv and venv listed in the output.

Don't see the CSV file?

Make sure you moved the file into the nids-project folder on your Desktop, not into the venv subfolder. The file should sit directly alongside the venv folder.

Help me find and move the CSV file

Why this specific file?

The CICIDS2017 dataset spans an entire week of network traffic. The Wednesday file has the richest attack diversity in a single day, with approximately 692,703 flows across five distinct attack types plus benign traffic. This gives your model enough variety to learn meaningful detection patterns.

Set your API key and verify the setup

The anthropic SDK automatically reads your API key from an environment variable called ANTHROPIC_API_KEY. You need to set this in your terminal session before running any scripts that call Claude.

  • Set your API key as an environment variable by running:

macOS

export ANTHROPIC_API_KEY=your-api-key-here

Windows

set ANTHROPIC_API_KEY=your-api-key-here
  • Replace your-api-key-here with the actual API key from your Anthropic account.

Where do I find my API key?

Log in to your Anthropic console at console.anthropic.com. Navigate to API Keys in the sidebar and create a new key if you do not have one already.

Keep your API key safe

Never share your API key or commit it to version control. This environment variable approach keeps the key out of your code files. The variable only lasts for your current terminal session.

Before you run the verification command, what do you think will print if everything installed correctly?

  • Verify that all packages are installed and your environment is working by running:
python -c "import sklearn; import anthropic; print(sklearn.__version__); print(anthropic.__version__)"

You should see two version numbers printed without any errors. The exact numbers will depend on what was latest when you installed.

Seeing a ModuleNotFoundError?

This means the packages did not install into your active environment. Check that (venv) still appears in your prompt. If it disappeared, reactivate the virtual environment and re-run the pip install command.

Help me fix my Python environment

Your environment is ready and your dataset is in place. Next up, you will train a Random Forest classifier that detects network attacks with high accuracy.

Train the Intrusion Detection Model

Your environment is ready with the dataset sitting in your project folder. Now you need the ML backbone that actually detects network attacks.

In this step, you will train a Random Forest classifier on the CICIDS2017 dataset. The model learns to distinguish benign traffic from five different attack types. Once trained, you will have a working detector that later steps can explain with an LLM.

In this step, get ready to:
  • Load and preprocess the CICIDS2017 Wednesday dataset for training.
  • Train a Random Forest classifier that detects five attack types with ~97% accuracy.
  • Extract and display the top 5 features the model uses to make decisions.
Load and preprocess the dataset

The raw CSV contains metadata columns (like IP addresses) that would cause data leakage if left in. It also has infinity values from division-by-zero calculations in the original traffic capture. You need to clean all of this before training.

  • Create a new file called nids_train.py in your project folder.
  • Add the imports and dataset loading code by pasting the following:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
import joblib

# Load dataset
print("Loading CICIDS2017 Wednesday dataset...")
df = pd.read_csv("Wednesday-workingHours.pcap_ISCX.csv")

# Strip whitespace from column names
df.columns = df.columns.str.strip()

# Drop metadata columns to prevent data leakage
df = df.drop(columns=["Flow ID", "Source IP", "Destination IP", "Timestamp"])

# Handle Inf and NaN values
df = df.replace([np.inf, -np.inf], np.nan)
df = df.dropna()

print(f"Dataset shape: {df.shape}")
print(f"Label distribution:\n{df['Label'].value_counts()}")

What does this code do?

  • Loads the full Wednesday CSV (~692,703 network flows) using pandas.
  • Strips whitespace from column names because the CSV has leading spaces in its headers (e.g., " Flow Duration" instead of "Flow Duration").
  • Drops Flow ID, Source IP, Destination IP, and Timestamp to prevent the model from memorizing specific connections instead of learning attack patterns.
  • Replaces infinity values with NaN and drops all NaN rows so the model receives only clean numeric data.
  • Save nids_train.py and run it to confirm the dataset loads correctly:
python nids_train.py

You should see the dataset shape printed (around 690,000 rows) and a label distribution showing BENIGN as the majority class with five attack types below it.

Seeing a FileNotFoundError?

  • Make sure Wednesday-workingHours.pcap_ISCX.csv is in the same folder as nids_train.py. The filename must match exactly, including the capital W and the hyphen.
  • Confirm your terminal is in the project folder by running ls and checking the CSV file is listed.

Help me debug a FileNotFoundError when loading the CICIDS2017 CSV.

Now add the feature engineering code below the last print statement. This encodes the text labels into numbers, scales all features to a common range, and splits the data for training.

  • Add this code at the bottom of nids_train.py:
# Encode labels
le = LabelEncoder()
df["Label_encoded"] = le.fit_transform(df["Label"])

# Separate features and target
X = df.drop(columns=["Label", "Label_encoded"])
y = df["Label_encoded"]

# Scale features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Stratified train/test split
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.2, random_state=42, stratify=y
)

What does this code do?

  • LabelEncoder converts text labels like "DoS Hulk" into integer class indices that the model can process.
  • StandardScaler normalizes each feature so they all have a mean of 0 and standard deviation of 1. This prevents features with large values (like byte counts) from dominating features with small values (like ratios).
  • The stratified split keeps the same proportion of each attack type in both training and test sets. random_state=42 makes the split reproducible.

Why use class_weight='balanced' instead of oversampling?

The dataset is heavily imbalanced. About 80% of flows are benign. Oversampling techniques like SMOTE create synthetic copies of minority classes, which risks leaking test data patterns into training.

Setting class_weight="balanced" tells the model to penalize misclassifying rare attack types more heavily. It achieves nearly the same accuracy without the data contamination risk.

Train the Random Forest classifier
  • Add the training and evaluation code at the bottom of nids_train.py:
# Train Random Forest
print("\nTraining Random Forest classifier...")
rf = RandomForestClassifier(
    n_estimators=100,
    class_weight="balanced",
    random_state=42,
    n_jobs=-1
)
rf.fit(X_train, y_train)

# Evaluate
y_pred = rf.predict(X_test)
print("\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=le.classes_))

What does this code do?

  • Creates a Random Forest with 100 decision trees (n_estimators=100). Each tree votes on whether a flow is malicious, and the majority wins.
  • n_jobs=-1 uses all your CPU cores for parallel training. This speeds up fitting on ~550,000 training rows.
  • classification_report prints precision, recall, and F1-score for each attack type so you can see where the model excels and where it struggles.

Before you run this, what accuracy do you think a 100-tree Random Forest will achieve on this dataset? Will it struggle with any particular attack type?

  • Save nids_train.py and run it:
python nids_train.py

You should see a classification report with overall accuracy around 97-98%. Each attack type (DoS Hulk, DoS GoldenEye, DoS Slowloris, DoS Slowhttptest, Heartbleed) shows its own precision and recall scores.

Training seems stuck or very slow?

  • Training 100 trees on ~550,000 rows can take 30-60 seconds depending on your machine. If it runs much longer, check that n_jobs=-1 is set so all CPU cores are used.
  • If you see a MemoryError, close other applications to free up RAM. The dataset plus scaled copy requires several hundred MB.

Help me troubleshoot slow Random Forest training on the CICIDS2017 dataset.

Extract feature importances and save the model

The model knows which features matter most for detecting attacks. Extracting these importances gives you a baseline for explaining predictions later.

  • Add the feature importance extraction and model saving code at the bottom of nids_train.py:
# Display top 5 feature importances
feature_names = X.columns.tolist()
importances = rf.feature_importances_
top_indices = np.argsort(importances)[-5:][::-1]
print("\nTop 5 Most Important Features:")
for i, idx in enumerate(top_indices, 1):
    print(f"  {i}. {feature_names[idx]}: {importances[idx]:.4f}")

# Save model artifacts
joblib.dump(rf, "rf_model.joblib")
joblib.dump(le, "label_encoder.joblib")
joblib.dump(scaler, "scaler.joblib")
joblib.dump(feature_names, "feature_names.joblib")
print("\nModel saved to rf_model.joblib")

What does this code do?

  • feature_importances_ is a property of trained Random Forests that ranks how much each feature contributed to correct classifications across all 100 trees.
  • The top 5 features (Flow Duration, Bwd Packet Length Mean, Flow IAT Max, Average Packet Size, Init Win Bytes Backward) are the strongest signals the model uses to distinguish attacks from normal traffic.
  • joblib serializes the model, encoder, scaler, and feature names to disk so you can reload them in later scripts without retraining.
  • Save nids_train.py and run the full script one final time:
python nids_train.py

At the end of the output, you should now see the top 5 features listed with their importance scores, followed by "Model saved to rf_model.joblib".

  • Confirm the model files were created by running:
ls *.joblib

You should see four files: rf_model.joblib, label_encoder.joblib, scaler.joblib, and feature_names.joblib.

Don't see all four .joblib files?

  • If the script threw an error before reaching the save section, scroll up in your terminal to find the error message. A common issue is a typo in variable names like feature_names or importances.
  • Make sure the save code is at the bottom of the file, after the training and evaluation code.

Help me figure out why my model artifacts aren't saving.

✔️ Awesome, I've got everything!

Great. Make sure you have saved nids_train.py and all four .joblib files are in your project folder.

ⓧ I'd like to double check the full code

Your nids_train.py should contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports (pandas, numpy, sklearn modules, joblib).
  • Dataset loading, column strip, metadata column drop, Inf/NaN handling, and shape print.
  • Label encoding, feature/target separation, StandardScaler, and stratified train/test split.
  • RandomForestClassifier creation, fitting, prediction, and classification report print.
  • Top 5 feature importance display and four joblib.dump calls saving the model, encoder, scaler, and feature names.

You now have a trained intrusion detection model that classifies five attack types with ~97% accuracy. Next up, you will see what happens when you show a raw flagged flow to a human analyst. Spoiler: it is a wall of meaningless numbers.

Inspect Flagged Flows Without Explanation

Your model from Step 2 classifies network flows with ~97% accuracy. But accuracy alone does not make a tool useful for humans.

When the model flags a flow as "DoS Slowloris", it outputs a label and a wall of raw numbers. A SOC analyst looking at this has no context on WHY it was flagged or WHAT to do about it. In this step, you will experience that gap firsthand.

In this step, get ready to:
  • Load the trained model and reconstruct the test set.
  • Print 5 flagged flows as raw feature values with predicted labels.
  • Observe the cryptic, analyst-unusable output that LLM explainability will fix.
Load the model and reconstruct the test set

To inspect what the model's predictions look like to a human, you need to load the saved model artifacts and recreate the exact same test set you evaluated against in Step 2.

  • Create a new file called nids_inspect.py in your project folder.
  • Add the following code to load model artifacts, reload the dataset with identical preprocessing, and reconstruct the test split:
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split
import joblib

# Load model artifacts
rf = joblib.load("rf_model.joblib")
le = joblib.load("label_encoder.joblib")
scaler = joblib.load("scaler.joblib")
feature_names = joblib.load("feature_names.joblib")

# Reload and preprocess dataset to get test set
df = pd.read_csv("Wednesday-workingHours.pcap_ISCX.csv")
df.columns = df.columns.str.strip()
df = df.drop(columns=["Flow ID", "Source IP", "Destination IP", "Timestamp"])
df = df.replace([np.inf, -np.inf], np.nan)
df = df.dropna()
df["Label_encoded"] = le.transform(df["Label"])

X = df.drop(columns=["Label", "Label_encoded"])
y = df["Label_encoded"]
X_scaled = scaler.transform(X)

X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.2, random_state=42, stratify=y
)

# Get predictions on test set
y_pred = rf.predict(X_test)

What does this code do?

This code reloads the exact same model and preprocessing pipeline you saved in Step 2. Using the same random_state=42 and stratify=y ensures you get the identical test set, so you can inspect the same flows the classification report evaluated.

Notice it uses le.transform() (not fit_transform()) because the label encoder was already fitted during training. Same for scaler.transform().

  • Save nids_inspect.py.
  • Confirm the model loads without errors by running:
python nids_inspect.py

You should see no output and no errors. The script loads the model and data silently, which confirms all four .joblib files loaded correctly.

Seeing a FileNotFoundError?

Make sure you are running the command from the same folder that contains rf_model.joblib and the CSV file. Run ls to verify they are all present in your current directory.

Help me debug my FileNotFoundError when loading model artifacts

Print flagged flows as raw feature walls

Now you will add the code that finds correctly-classified malicious flows and prints their raw feature values. This is the output a human analyst would see from a traditional ML pipeline.

  • In nids_inspect.py, add the following code below the y_pred = rf.predict(X_test) line:
# Find correctly classified malicious flows (one per attack type)
malicious_mask = (y_pred != le.transform(["BENIGN"])[0]) & (y_pred == y_test.values)
malicious_indices = np.where(malicious_mask)[0]

print("=" * 70)
print("RAW FLAGGED FLOWS (No Explanation)")
print("=" * 70)
print("\nThis is what an analyst sees without LLM explainability:\n")

shown_labels = set()
count = 0
for idx in malicious_indices:
    predicted_label = le.inverse_transform([y_pred[idx]])[0]
    if predicted_label in shown_labels:
        continue
    shown_labels.add(predicted_label)
    count += 1

    flow_features = X_test[idx]
    print(f"\n--- Alert #{count} ---")
    print(f"Predicted: {predicted_label}")
    print(f"Raw features (top 5 by importance):")
    importances = rf.feature_importances_
    top_indices = np.argsort(importances)[-5:][::-1]
    for fi in top_indices:
        print(f"  {feature_names[fi]}: {flow_features[fi]:.4f} (scaled)")
    print()

    if count >= 5:
        break

What does this code do?

  • malicious_mask filters to flows that are both non-BENIGN AND correctly classified (prediction matches actual label). This ensures you are looking at real detections, not false positives.
  • The shown_labels set ensures you get one flow per attack type, giving variety across all 5 attack categories.
  • For each flow, the loop prints only the top 5 features by global importance. These are standardized values (where 0 is the dataset mean), not the original raw values.
  • Below the loop, add the closing summary that names the problem explicitly:
print("\n" + "=" * 70)
print("PROBLEM: These numbers are meaningless to a human analyst.")
print("They need to know WHY it was flagged and WHAT to do about it.")
print("=" * 70)

Why print the problem statement?

This closing message is intentional. It makes the script self-documenting about its own limitation. In the next step, you will replace this dead-end with Claude-powered explanations that answer both questions.

Run the script and observe the explainability gap

Before you run this, ask yourself: if you were a junior SOC analyst and the system handed you a predicted label plus five standardized numbers, would you know what action to take?

  • Save nids_inspect.py and run it:
python nids_inspect.py

You should see 5 alerts printed, each with a predicted label and a wall of scaled numbers. The output looks something like this:

--- Alert #1 ---
Predicted: DoS Hulk
Raw features (top 5 by importance):
  Flow Duration: -0.0842 (scaled)
  Bwd Packet Length Mean: 0.3219 (scaled)
  Flow IAT Max: -0.1153 (scaled)
  Average Packet Size: 0.5027 (scaled)
  Init Win Bytes Backward: -0.2841 (scaled)

What should I see?

Five alerts, one per attack type (DoS Hulk, DoS GoldenEye, DoS Slowloris, DoS Slowhttptest, Heartbleed). Each shows 5 feature values as decimals near zero. The exact numbers will vary, but they are all standardized values with no units or human-readable context.

The closing line reads: "PROBLEM: These numbers are meaningless to a human analyst." That is the designed obstacle. You are experiencing exactly what a real SOC analyst would experience using a traditional ML-only pipeline.

Seeing fewer than 5 alerts?

If you see fewer than 5 attack types, that means the model misclassified all flows for a rare attack type (likely Heartbleed, which has very few samples). This is expected behavior with imbalanced data. The important thing is that you see multiple alerts with cryptic numbers.

Help me understand why my inspection script shows fewer than 5 attack types

✔️ Awesome, I've got everything!

Great. Double check you have saved nids_inspect.py and confirmed the cryptic output in your terminal.

ⓧ I'd like to double check the full code

Here is the complete nids_inspect.py file. Scroll up and confirm each section is present in order: imports and loads, data reload and preprocessing, train/test split and predictions, the filter and print loop, and the closing problem statement.

This is the explainability gap. The model knows a flow is malicious, but it cannot tell a human why. In the next step, you will connect Claude to translate these raw numbers into plain-English analyst reports.

Add LLM-Powered Explanations

In the last step, you saw what a human analyst actually receives from the model: a wall of scaled numbers and a label like "DoS Slowloris." No context, no reasoning, no suggested action.

This step fixes that gap. You will connect Claude to your detection pipeline so that every flagged flow gets a plain-English explanation a junior SOC analyst can act on immediately.

In this step, get ready to:
  • Create a new pipeline script that initializes the Anthropic client alongside your trained model.
  • Write an explain_detection function that sends flow data to Claude with a cybersecurity-analyst system prompt.
  • Generate natural-language explanations for 5 flagged flows and compare the output to Step 3's raw numbers.
Create the pipeline script with Claude

The new file will combine your saved model artifacts with the Anthropic SDK to form a detection-plus-explanation pipeline.

  • Create a new file called nids_pipeline.py in your project folder.
  • Add the initial setup code that loads the model, initializes the Anthropic client, and reconstructs the test set:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
import joblib
from anthropic import Anthropic

# Load model artifacts
rf = joblib.load("rf_model.joblib")
le = joblib.load("label_encoder.joblib")
scaler = joblib.load("scaler.joblib")
feature_names = joblib.load("feature_names.joblib")

# Initialize Anthropic client
client = Anthropic()

# Reload dataset and get test set
df = pd.read_csv("Wednesday-workingHours.pcap_ISCX.csv")
df.columns = df.columns.str.strip()
df = df.drop(columns=["Flow ID", "Source IP", "Destination IP", "Timestamp"])
df = df.replace([np.inf, -np.inf], np.nan)
df = df.dropna()
df["Label_encoded"] = le.transform(df["Label"])

X = df.drop(columns=["Label", "Label_encoded"])
y = df["Label_encoded"]
X_scaled = scaler.transform(X)

X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.2, random_state=42, stratify=y
)

What does this setup do?

This is largely the same loading logic from nids_inspect.py, with one key addition: from anthropic import Anthropic brings in the SDK that talks to Claude.

The Anthropic() client automatically reads your ANTHROPIC_API_KEY environment variable. No manual key passing needed.

  • Save nids_pipeline.py.
  • Confirm the setup loads correctly by running:
python nids_pipeline.py

The script should run and exit without errors. It takes a moment to load the dataset, then finishes silently.

Seeing a ModuleNotFoundError?

If you see ModuleNotFoundError: No module named 'anthropic', make sure your virtual environment is activated before running the script.

If the error mentions a missing .joblib file, confirm you ran nids_train.py in Step 2 and the four .joblib files exist in your project folder.

Help me debug my pipeline setup.

Write the explain_detection function

This function is the bridge between raw ML output and human-readable intelligence. It packages a flow's key features into a structured prompt and asks Claude to explain the detection.

  • Add the following function below the train_test_split call in nids_pipeline.py:
def explain_detection(flow_features_scaled, predicted_label, feature_names, importances):
    """Send flagged flow to Claude for plain-English explanation."""
    top_indices = np.argsort(importances)[-5:][::-1]
    feature_summary = "\n".join(
        f"- {feature_names[i]}: {flow_features_scaled[i]:.4f} (standardized)"
        for i in top_indices
    )

    message = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=300,
        system="You are a cybersecurity analyst explaining IDS alerts to junior SOC analysts. Given a flagged network flow's key features and the predicted attack type, explain in 3-4 sentences: (1) why this flow was flagged, (2) what the abnormal feature values indicate about the attack, and (3) one recommended action. Be specific about the feature values. Do not use jargon without explaining it.",
        messages=[
            {
                "role": "user",
                "content": f"A network flow was classified as: {predicted_label}\n\nTop features driving this detection (standardized values, where 0 = dataset mean):\n{feature_summary}\n\nExplain this detection for a junior analyst."
            }
        ]
    )
    return message.content[0].text

What does this function do?

  • It extracts the top 5 features by global importance and formats them with their standardized values for this specific flow.
  • The system prompt instructs Claude to act as a cybersecurity analyst who explains alerts to junior staff. It requires three things: why the flow was flagged, what the values mean, and one recommended action.
  • The user message gives Claude the predicted attack label plus the feature values, anchoring "0 = dataset mean" so Claude understands that large positive or negative values are abnormal.
  • The function returns only the text of Claude's response, ready to print.

Why claude-haiku-4-5?

The explanations are short (3-4 sentences) and highly structured. claude-haiku-4-5 is fast and inexpensive for this kind of constrained output. The full pipeline calling it 5 times costs well under a cent.

  • Save nids_pipeline.py.
Generate explanations for flagged flows

Now you will wire the function into a loop that classifies the test set, picks one malicious flow per attack type, and prints each with its Claude-generated explanation.

  • Add the following code at the bottom of nids_pipeline.py, after the explain_detection function:
# Classify test batch
y_pred = rf.predict(X_test)
benign_label = le.transform(["BENIGN"])[0]
malicious_mask = y_pred != benign_label
malicious_indices = np.where(malicious_mask)[0]
importances = rf.feature_importances_
shown_labels = set()
alert_num = 0

for idx in malicious_indices:
    predicted_label = le.inverse_transform([y_pred[idx]])[0]
    if predicted_label in shown_labels:
        continue
    shown_labels.add(predicted_label)
    alert_num += 1

    explanation = explain_detection(
        X_test[idx], predicted_label, feature_names, importances
    )
    print(f"Alert #{alert_num}: {predicted_label}")
    print(f"Explanation: {explanation}")
    print()

    if alert_num >= 5:
        break

What does this loop do?

  • It classifies the entire test set, then filters to only malicious predictions using the benign label as a mask.
  • The shown_labels set ensures you get one example per attack type (DoS Hulk, DoS GoldenEye, Slowloris, Slowhttptest, Heartbleed) instead of five of the same kind.
  • For each unique attack, it calls explain_detection and prints the label alongside Claude's explanation.
  • Save nids_pipeline.py.

Before you run this, think back to Step 3's output. What do you expect to see now that Claude is interpreting those same flows?

  • Run the pipeline:
python nids_pipeline.py

You should see 5 alerts print, each with its attack type and a 3-4 sentence explanation naming the specific features that drove the detection and recommending an action.

What should I see?

Each alert now reads like a human-written brief. For example, a DoS Slowloris detection might explain that the flow has an abnormally long duration with tiny packet sizes, consistent with an attacker holding connections open to exhaust server resources.

Compare this to Step 3's output. The same flows that were cryptic walls of numbers are now actionable intelligence a junior analyst can respond to.

Seeing an authentication error?

If you see an error about an invalid API key, confirm your ANTHROPIC_API_KEY environment variable is set in the same terminal session where you are running the script.

If the script takes a long time, that is normal. Each of the 5 API calls waits for Claude to respond, which can take a few seconds each.

Help me fix my Claude API call.

✔️ Awesome, I've got everything!

Great. Double check you have saved nids_pipeline.py with all three sections: the setup, the explain_detection function, and the classification loop.

ⓧ I'd like to double check the full code

Scroll up through your file and confirm these sections appear in order:

  • Imports and model loading (imports, joblib.load calls, client = Anthropic()).
  • Dataset reload and train/test split (same preprocessing as nids_inspect.py).
  • The explain_detection function with the system prompt and user message template.
  • The classification loop that iterates malicious indices and prints each alert with its explanation.

Your model now speaks human. Each flagged flow comes with context, reasoning, and a suggested next step. In the next step, you will assemble everything into a production-style batch report that an analyst can scan at a glance.

Build the Full Analysis Pipeline

In the previous step, you connected Claude to your model and saw individual explanations appear alongside each flagged flow. That proved the concept works.

A real SOC analyst does not process one flow at a time. They need a batch report they can scan in seconds: how many flows came in, how many are malicious, what types of attacks appeared, and then the detailed alerts below. In this step, you will transform your script into a production-style pipeline that outputs a formatted threat report.

In this step, get ready to:
  • Replace the basic output loop with a summary report header showing flow counts and attack breakdown.
  • Add formatted alert cards with confidence scores and LLM explanations.
  • Run the complete pipeline and verify the threat report output.
Add the report header and summary statistics

The current bottom section of nids_pipeline.py prints bare alerts with no context. You will replace it with a structured report that first summarizes the entire batch before diving into details.

  • In nids_pipeline.py, find the comment # Classify test batch (below the explain_detection function).
  • Delete everything from that comment to the end of the file.
  • Replace it with the following code:
# Simulate analyzing a batch of flows
print("=" * 70)
print("NETWORK INTRUSION DETECTION - THREAT REPORT")
print("=" * 70)

# Classify test batch
y_pred = rf.predict(X_test)
benign_label = le.transform(["BENIGN"])[0]
malicious_mask = y_pred != benign_label
total_flows = len(y_pred)
malicious_count = malicious_mask.sum()
benign_count = total_flows - malicious_count

print(f"\nFlows analyzed: {total_flows}")
print(f"Benign: {benign_count}")
print(f"Malicious: {malicious_count}")
print(f"\nAttack breakdown:")
for label_idx in np.unique(y_pred[malicious_mask]):
    label_name = le.inverse_transform([label_idx])[0]
    count = (y_pred[malicious_mask] == label_idx).sum()
    print(f"  {label_name}: {count}")

What does this code do?

  • The report header prints a clear title so the output reads like a professional document, not raw debug logs.
  • classify_report = rf.predict(X_test) classifies every flow in the test set, simulating a batch of incoming network traffic.
  • The summary statistics count total, benign, and malicious flows, then break malicious ones down by attack type so an analyst can see the threat landscape at a glance.
  • Save nids_pipeline.py.
  • Run the script to confirm the summary section works by running:
python nids_pipeline.py

You should see the report header, total flow counts, and an attack breakdown listing each attack type with its count. The script ends after the summary because you have not added the alert cards yet.

Seeing a KeyError or empty breakdown?

  • Make sure you deleted the old loop completely. Leftover lines from the previous version can shadow the new malicious_mask variable.
  • Confirm rf_model.joblib is in the same folder as nids_pipeline.py.

Help me debug my nids_pipeline.py report header.

Add formatted alert cards with explanations

The summary gives the big picture. Now you need the detailed alerts that tell an analyst exactly what happened and why.

  • In nids_pipeline.py, add the following code directly below the attack breakdown loop you just pasted:
print("\n" + "-" * 70)
print("DETAILED ALERTS (showing 5 examples with LLM explanations)")
print("-" * 70)

# Generate explanations for 5 diverse malicious flows
malicious_indices = np.where(malicious_mask)[0]
importances = rf.feature_importances_
shown_labels = set()
alert_num = 0

for idx in malicious_indices:
    predicted_label = le.inverse_transform([y_pred[idx]])[0]
    if predicted_label in shown_labels:
        continue
    shown_labels.add(predicted_label)
    alert_num += 1

    print(f"\n  ALERT #{alert_num}")
    print(f"  Attack Type: {predicted_label}")
    print(f"  Confidence: {rf.predict_proba(X_test[idx].reshape(1, -1)).max():.1%}")

    explanation = explain_detection(
        X_test[idx], predicted_label, feature_names, importances
    )
    print(f"  Explanation: {explanation}")
    print()

    if alert_num >= 5:
        break

print("=" * 70)
print("END OF REPORT")
print("=" * 70)

What does this code do?

  • The loop picks one flow per attack type (using shown_labels to deduplicate) so the report covers the full range of threats detected.
  • predict_proba returns the probability the Random Forest assigned to each class. Taking .max() gives the confidence for the predicted label.
  • Each alert card shows the attack type, model confidence, and Claude's plain-English explanation. This is the analyst-ready format that makes raw ML output actionable.
  • Save nids_pipeline.py.

Seeing an IndentationError?

  • Make sure the new code starts at column 0 (no extra indentation). It should be at the same level as the report header code above it.
  • Check that the for loop body is indented with 4 spaces consistently.

Help me fix my alert card loop in nids_pipeline.py.

Run the full pipeline

Your pipeline is complete. Before you run it, think about what you expect to see.

  • What do you think the output will look like? Consider how many sections the report has and what each one shows.
  • Run the full pipeline by running:
python nids_pipeline.py

You should see a formatted threat report with three sections: the report header, the summary (total flows analyzed, benign and malicious counts, attack type breakdown), and then 5 detailed alert cards. Each alert card shows the attack type, a confidence percentage, and a multi-sentence explanation from Claude.

What should the output look like?

The report starts with a banner of = characters and the title. The summary shows roughly 138,000 total flows with around 27,000 flagged as malicious. The attack breakdown lists each type (DoS GoldenEye, DoS Hulk, DoS Slowhttptest, DoS Slowloris, Heartbleed) with counts.

Each alert card includes a confidence score (typically 80-99%) and 3-4 sentences from Claude explaining why that specific flow was flagged. The explanations name specific features and recommend actions like blocking the source IP or rate-limiting connections.

API calls take a few seconds

The script calls Claude 5 times (once per alert card). Expect 10-20 seconds of total runtime depending on network latency. Each call costs a fraction of a cent with claude-haiku-4-5.

✔️ Awesome, I've got everything!

Great. Double check you have saved nids_pipeline.py and that your report printed all five alert cards with explanations.

ⓧ I'd like to double check the full code

Your nids_pipeline.py should contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports (pandas, numpy, train_test_split, joblib, Anthropic).
  • Model artifact loading (rf_model.joblib, label_encoder.joblib, scaler.joblib, feature_names.joblib).
  • Anthropic client initialization (client = Anthropic()).
  • Dataset reload and preprocessing (read CSV, strip columns, drop metadata, handle Inf/NaN, encode labels, scale, train/test split).
  • The explain_detection function calling claude-haiku-4-5.
  • Report header printing NETWORK INTRUSION DETECTION - THREAT REPORT.
  • Summary statistics (total flows, benign, malicious, attack breakdown loop).
  • Detailed alerts section header.
  • Alert loop with predict_proba confidence and explain_detection call.
  • END OF REPORT footer.

Your pipeline now takes raw network flows, classifies them with a Random Forest, and produces a formatted threat report with AI-powered explanations. In the Secret Mission, you will swap the generic global feature importances for per-prediction SHAP values to make Claude's explanations mathematically precise.

Secret mission

Add SHAP-Powered Feature Attribution

Your current pipeline uses global feature importances to explain every prediction. That means Claude gets the same top-5 features regardless of which specific flow it is explaining. SHAP values give you per-prediction local explanations, so Claude knows exactly which features pushed THIS flow toward malicious.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing cloud costs. The only expense was the Anthropic API calls you already made (approximately $0.05 total).

Resources you used:

  • Project folder on your Desktop (Python scripts, model artifacts, dataset CSV).
  • Virtual environment with scikit-learn, pandas, anthropic, and shap packages.
  • ANTHROPIC_API_KEY environment variable.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the pipeline, trying different prompts, or extending the SHAP analysis.

  • Your project folder stays on your Desktop with all scripts and model artifacts intact.
  • Your virtual environment remains available for future runs.
  • Your ANTHROPIC_API_KEY stays set in your current terminal session (it will clear when you close the terminal).

Pause - I'll come back to this later

Shut down the virtual environment to free up your terminal. Your files stay on disk and you can reactivate later.

  • Deactivate the virtual environment by running:
deactivate
  • When you come back, navigate to your project folder and reactivate with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
  • You will need to re-export your ANTHROPIC_API_KEY environment variable in the new terminal session.

Delete - I don't want to use this again

Remove all project files, the virtual environment, and the dataset. This frees up disk space (the CSV alone is approximately 200 MB).

  • Deactivate the virtual environment if it is still active:
deactivate
  • Navigate to your Desktop and delete the entire project folder.

macOS

cd ~/Desktop
rm -rf nids-explainability

Windows

cd ~\Desktop
rmdir /s /q nids-explainability
  • Unset the environment variable in your current terminal session:

macOS

unset ANTHROPIC_API_KEY

Windows

set ANTHROPIC_API_KEY=
  • This removes all Python scripts, trained model files, the dataset CSV, and the virtual environment with all installed packages.

Nice Work!

Nice Work!

Nice work! You just built a complete cybersecurity analysis pipeline that detects network intrusions with machine learning and explains its findings in plain English using Claude.

You've learned how to:

  • Train a Random Forest classifier on the CICIDS2017 dataset that detects 5 network attack types with ~97%+ accuracy.
  • Identify the usability gap in raw ML output and understand why human analysts cannot act on feature vectors and numeric predictions alone.
  • Build an LLM explainability layer that calls Claude to translate model predictions into structured, actionable threat intelligence a junior SOC analyst can use.
  • Assemble a full detection-to-explanation pipeline that classifies network flows in batch and outputs formatted threat reports with summary statistics and individual alert cards.
  • Secret Mission: Replace global feature importances with SHAP values for mathematically grounded per-prediction explanations that reference exact feature contributions.

Ready to quiz yourself?