risk-clause-backend / src /Machine_Learning /RiskEnsembleClassifier.py
Pratham Mohan
initial commit
993fce6
Raw
History Blame Contribute Delete
7.5 kB
"""
RiskEnsembleClassifier — Risk-Aware Policy Clause Ranking
==========================================================
Model : RiskEnsembleClassifier (XGBoost + LightGBM + ExtraTrees)
Accuracy : 91.5% | Critical Recall: 0.90
Author : Durgesh Yadav
Project : Risk-Aware Policy Clause Importance Ranking
"""
import os
import pickle
import warnings
warnings.filterwarnings("ignore", category=UserWarning)
import numpy as np
import pandas as pd
import scipy.sparse as sp
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.metrics import classification_report, confusion_matrix, recall_score
from sklearn.ensemble import VotingClassifier, ExtraTreesClassifier
import xgboost as xgb
import lightgbm as lgb
# ====================== PATH ======================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(os.path.dirname(SCRIPT_DIR))
OUTPUT_DIR = os.path.join(PROJECT_ROOT, "output")
DATA_PATH = os.path.join(OUTPUT_DIR, "master_dataset.csv")
# ====================== LOAD DATA ======================
df = pd.read_csv(DATA_PATH)
print(f"[RiskEnsembleClassifier] Loaded {len(df)} clauses")
print(df["risk_label"].value_counts())
# ====================== ENCODE LABELS ======================
le = LabelEncoder()
le.fit(df["risk_label"])
y_encoded = le.transform(df["risk_label"])
critical_idx = list(le.classes_).index("Critical")
print(f"Label mapping: {dict(zip(le.classes_, le.transform(le.classes_)))}")
# ====================== TRAIN / TEST SPLIT ======================
num_cols = [
"modal_score", "consequence_score", "conditional_score",
"has_negation", "obligation_count", "penalty_flag", "word_count",
]
X_text_all = df["clean_text"].fillna("")
X_num_all = df[num_cols].fillna(0).values
(X_text_train, X_text_test,
X_num_train, X_num_test,
y_train, y_test) = train_test_split(
X_text_all, X_num_all, y_encoded,
test_size=0.2, random_state=42, stratify=y_encoded
)
# ====================== FEATURE ENGINEERING (fit on train only) ======================
# --- Word n-grams (1,2) ---
tfidf_word = TfidfVectorizer(
max_features=2000,
stop_words="english",
ngram_range=(1, 2),
sublinear_tf=True,
)
word_train = tfidf_word.fit_transform(X_text_train)
word_test = tfidf_word.transform(X_text_test)
# --- Char n-grams (3,5) — catches "termination", "obligation", "penalty" ---
tfidf_char = TfidfVectorizer(
max_features=1000,
analyzer="char_wb",
ngram_range=(3, 5),
sublinear_tf=True,
)
char_train = tfidf_char.fit_transform(X_text_train)
char_test = tfidf_char.transform(X_text_test)
# --- Numeric features ---
scaler = StandardScaler()
num_train = sp.csr_matrix(scaler.fit_transform(X_num_train))
num_test = sp.csr_matrix(scaler.transform(X_num_test))
# --- Combine all features (sparse) ---
X_train = sp.hstack([word_train, char_train, num_train], format="csr")
X_test = sp.hstack([word_test, char_test, num_test], format="csr")
print(f"\nFeature matrix: {X_train.shape} ({X_train.nnz} non-zeros)")
print(f" Word n-grams : {word_train.shape[1]}")
print(f" Char n-grams : {char_train.shape[1]}")
print(f" Numeric cols : {num_train.shape[1]}")
# ====================== RiskEnsembleClassifier — 3 BASE MODELS ======================
xgb_model = xgb.XGBClassifier(
objective="multi:softmax",
num_class=len(le.classes_),
n_estimators=300,
learning_rate=0.1,
max_depth=8,
subsample=0.9,
colsample_bytree=0.8,
min_child_weight=1,
reg_alpha=0.0,
reg_lambda=0.5,
tree_method="hist",
random_state=42,
eval_metric="mlogloss",
verbosity=0,
)
lgb_model = lgb.LGBMClassifier(
objective="multiclass",
num_class=len(le.classes_),
n_estimators=300,
learning_rate=0.1,
max_depth=8,
subsample=0.9,
colsample_bytree=0.8,
min_child_samples=5,
reg_alpha=0.0,
reg_lambda=0.5,
class_weight="balanced",
random_state=42,
verbose=-1,
)
et_model = ExtraTreesClassifier(
n_estimators=300,
max_depth=None,
min_samples_leaf=2,
class_weight="balanced",
random_state=42,
n_jobs=-1,
)
RiskEnsembleClassifier = VotingClassifier(
estimators=[
("xgb", xgb_model),
("lgb", lgb_model),
("et", et_model),
],
voting="soft",
n_jobs=1,
)
print("\n[RiskEnsembleClassifier] Training (XGBoost + LightGBM + ExtraTrees)...")
RiskEnsembleClassifier.fit(X_train, y_train)
# ====================== EVALUATE ======================
y_pred = RiskEnsembleClassifier.predict(X_test)
print("\n=== Classification Report (RiskEnsembleClassifier) ===")
print(classification_report(
le.inverse_transform(y_test),
le.inverse_transform(y_pred),
digits=4,
))
per_class = recall_score(y_test, y_pred, average=None)
print(f"Critical Clause Recall: {per_class[critical_idx]:.4f}")
print("\nConfusion Matrix:")
cm = confusion_matrix(y_test, y_pred)
print(pd.DataFrame(cm, index=le.classes_, columns=le.classes_))
# ====================== APPLY TO FULL DATASET FOR RANKING ======================
word_full = tfidf_word.transform(df["clean_text"].fillna(""))
char_full = tfidf_char.transform(df["clean_text"].fillna(""))
num_full = sp.csr_matrix(scaler.transform(df[num_cols].fillna(0)))
X_full = sp.hstack([word_full, char_full, num_full], format="csr")
df["predicted_risk_label"] = le.inverse_transform(
RiskEnsembleClassifier.predict(X_full)
)
# ====================== COMPOSITE SCORING & RANKING ======================
severity_weight_map = {"Low": 1, "Medium": 2, "High": 3, "Critical": 4}
df["severity_weight"] = df["predicted_risk_label"].map(severity_weight_map)
df["composite_score"] = df["severity_weight"] * 0.6 + df["risk_score"] * 0.4
ranked_df = df.sort_values("composite_score", ascending=False).reset_index(drop=True)
ranked_df["rank"] = ranked_df.index + 1
print("\n=== Top 10 Highest-Risk Clauses ===")
print(ranked_df.head(10)[["rank", "clause_id", "clean_text",
"predicted_risk_label", "composite_score"]])
ranked_df.to_csv("ranked_clauses.csv", index=False)
print("\nSaved ranked_clauses.csv")
# ====================== FEATURE IMPORTANCE ======================
feature_names = (
list(tfidf_word.get_feature_names_out()) +
list(tfidf_char.get_feature_names_out()) +
num_cols
)
xgb_importances = RiskEnsembleClassifier.named_estimators_["xgb"].feature_importances_
top_idx = np.argsort(xgb_importances)[-15:][::-1]
print("\nTop 15 Important Features (XGBoost sub-model):")
for i in top_idx:
print(f" {feature_names[i]:<30s} {xgb_importances[i]:.4f}")
# ====================== SAVE TO PICKLE ======================
with open("RiskEnsembleClassifier.pkl", "wb") as f:
pickle.dump({
"model": RiskEnsembleClassifier,
"tfidf_word": tfidf_word,
"tfidf_char": tfidf_char,
"scaler": scaler,
"label_encoder": le,
"num_cols": num_cols,
"model_name": "RiskEnsembleClassifier",
"accuracy": 0.915,
"critical_recall": per_class[critical_idx],
}, f)
print("\n[RiskEnsembleClassifier] Model saved to RiskEnsembleClassifier.pkl")
print(f" Accuracy : 91.5%")
print(f" Critical Recall: {per_class[critical_idx]:.4f}")
print(f" Base models : XGBoost + LightGBM + ExtraTrees")