TemporalDrift-ETM / preprocess.py
RabbeTota's picture
initial commit
23fd13d verified
Raw
History Blame Contribute Delete
7.63 kB
"""
preprocess.py - TemporalDrift-ETM
=====================================
Converts any NTLFlowLyzer-compatible CSV into the fixed 35-feature
scaled input that the ensemble model expects.
The model was trained on 35 features selected by Random Forest importance
ranking from an original 76-column feature space. Uploaded CSV files may
have any number of columns (40, 50, 70, 85, ...). This module handles:
- Any number of input columns
- Columns that are metadata, identifiers, or labels -> silently skipped
- Selected features present in the CSV -> cleaned and scaled
- Selected features absent from the CSV -> filled with 0.0
- Non-numeric cell values -> coerced to column mean
- Inf / -Inf values -> replaced with column mean
The per-feature scaling parameters (data_min, scale) are extracted from
the training MinMaxScaler at notebook-save time and stored in scaler_35.pkl,
so this module never requires the full 76-feature scaler at inference time.
Standalone usage
----------------
python preprocess.py input.csv output.csv [--models models/]
Module usage (from app.py)
--------------------------
from preprocess import load_scaler_35, prepare_features
scaler_35 = load_scaler_35("models/scaler_35.pkl")
X, present, miss = prepare_features(df, scaler_35)
"""
import os
import sys
import numpy as np
import pandas as pd
import joblib
# ---------------------------------------------------------------------------
# Column names that are never network-flow features.
# Any CSV column in this set is silently ignored during feature extraction.
# ---------------------------------------------------------------------------
NON_FEATURE_COLS = frozenset({
"Flow ID",
"Src IP",
"Src Port",
"Dst IP",
"Dst Port",
"Protocol",
"Timestamp",
"Malware Family",
"Label",
"label",
"session",
"Session",
"Prediction",
})
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def load_scaler_35(path):
"""
Load the 35-feature scaler dict produced by the notebook save cell.
The dict contains three keys:
feature_names : list[str] - the 35 selected feature names (in order)
data_min : np.ndarray - training minimum per feature, shape (35,)
scale : np.ndarray - 1/(max-min) per feature, shape (35,)
Parameters
----------
path : str
Path to scaler_35.pkl.
Returns
-------
dict
"""
artifact = joblib.load(path)
required = {"feature_names", "data_min", "scale"}
missing = required - set(artifact.keys())
if missing:
raise ValueError(
f"scaler_35.pkl is missing keys: {missing}. "
"Re-run the updated notebook save cell to regenerate it."
)
return artifact
def prepare_features(df, scaler_35):
"""
Convert a DataFrame with any columns into the 35-feature scaled matrix.
For each of the 35 model features:
- Column present in df -> coerce to float, replace non-finite values
with the column mean, apply MinMaxScaler
formula: (x - data_min) * scale, clip to [0,1].
- Column absent from df -> fill the entire column with 0.0 in scaled
space (neutral value that does not push the
model toward any particular prediction).
Parameters
----------
df : pd.DataFrame
Raw input data (any number of columns, any column names).
scaler_35 : dict
Output of load_scaler_35().
Returns
-------
X_scaled : np.ndarray, shape (n_samples, 35)
Model-ready scaled feature matrix.
present : list[str]
Names of the 35 features that were found in df.
missing : list[str]
Names of the 35 features that were absent in df (filled with 0.0).
"""
feat_names = scaler_35["feature_names"]
data_min = np.asarray(scaler_35["data_min"], dtype=np.float64)
scale = np.asarray(scaler_35["scale"], dtype=np.float64)
n = len(df)
X = np.zeros((n, len(feat_names)), dtype=np.float64)
present = []
missing = []
for i, feat in enumerate(feat_names):
if feat not in df.columns:
missing.append(feat)
# Column stays at 0.0 - neutral fill in scaled space.
continue
col = pd.to_numeric(df[feat], errors="coerce")
# Replace non-finite values with the finite column mean.
finite_vals = col[np.isfinite(col)]
col_mean = float(finite_vals.mean()) if len(finite_vals) > 0 else 0.0
col = col.fillna(col_mean)
col = col.replace([np.inf, -np.inf], col_mean)
# MinMaxScaler transform: X_scaled = (X - data_min) * scale
# This exactly replicates sklearn's MinMaxScaler.transform()
# but for a single feature at a time.
X[:, i] = (col.to_numpy(dtype=np.float64) - data_min[i]) * scale[i]
present.append(feat)
# Clip to [0, 1] as sklearn's MinMaxScaler.transform() does.
np.clip(X, 0.0, 1.0, out=X)
return X, present, missing
def report_coverage(present, missing, file=None):
"""
Print a human-readable feature coverage summary.
Parameters
----------
present : list[str]
missing : list[str]
file : file object or None (default: stdout)
"""
total = len(present) + len(missing)
print(f"Feature coverage: {len(present)}/{total} columns found in CSV.", file=file)
if missing:
print(f" {len(missing)} feature(s) absent - filled with 0 in scaled space:",
file=file)
for m in missing:
print(f" - {m}", file=file)
# ---------------------------------------------------------------------------
# Standalone CLI
# ---------------------------------------------------------------------------
def _main():
import argparse
parser = argparse.ArgumentParser(
description=(
"Convert any NTLFlowLyzer-compatible CSV to the 35-feature "
"scaled format expected by TemporalDrift-ETM."
)
)
parser.add_argument("input", help="Path to the raw input CSV file")
parser.add_argument("output", help="Path to write the processed output CSV")
parser.add_argument(
"--models", default="models",
help="Directory containing scaler_35.pkl (default: models/)",
)
args = parser.parse_args()
scaler_35_path = os.path.join(args.models, "scaler_35.pkl")
if not os.path.exists(scaler_35_path):
print(
f"ERROR: {scaler_35_path} not found.\n"
"Re-run the updated notebook save cell to generate scaler_35.pkl.",
file=sys.stderr,
)
sys.exit(1)
print(f"Reading {args.input} ...")
df = pd.read_csv(args.input)
print(f" {len(df):,} rows | {df.shape[1]} columns")
scaler_35 = load_scaler_35(scaler_35_path)
feat_names = scaler_35["feature_names"]
print(f"\nPreparing {len(feat_names)} model features ...")
X, present, missing = prepare_features(df, scaler_35)
report_coverage(present, missing)
out_df = pd.DataFrame(X, columns=feat_names)
out_df.to_csv(args.output, index=False)
print(f"\nSaved: {args.output}")
print(f"Output shape: {out_df.shape}")
if __name__ == "__main__":
_main()