Pratham Mohan
initial commit
993fce6
|
Raw
History Blame Contribute Delete
8.89 kB

RiskEnsembleClassifier

Risk-Aware Policy Clause Importance Ranking β€” Durgesh Yadav: Risk Modeling & ML Lead


What This Does

Takes a policy document's clauses and automatically predicts the risk severity of each clause (Low / Medium / High / Critical), then ranks them by risk so the most dangerous clauses appear first.


Folder Structure

src/
└── Machine_Learning/
    β”œβ”€β”€ RiskEnsembleClassifier.py   ← main training + evaluation script
    β”œβ”€β”€ test_model.py               ← testing script (csv / interactive / error analysis)
    β”œβ”€β”€ RiskEnsembleClassifier.pkl  ← saved model bundle (gitignored if large)
    β”œβ”€β”€ ranked_clauses.csv          ← output: all clauses ranked by risk score
    └── requirements.txt

How to Run

# Install dependencies
pip install -r requirements.txt

# Train and evaluate the model
python src/Machine_Learning/RiskEnsembleClassifier.py

# Or with custom data path
python src/Machine_Learning/RiskEnsembleClassifier.py --data "path/to/dataset.csv"

Models Trained

Three baseline models were trained and compared before arriving at the final ensemble.

Model 1 β€” Logistic Regression (Baseline)

Simple linear classifier used as the starting reference point.

Class Precision Recall F1-Score Support
Critical 0.7823 0.6950 0.7361 200
High 0.6912 0.7150 0.7029 200
Low 0.7234 0.8100 0.7644 200
Medium 0.6978 0.6700 0.6836 200
Overall Accuracy 0.7225 800

Critical Clause Recall: 0.6950


Model 2 β€” Random Forest

Ensemble of decision trees, better than logistic regression but still limited by bag-of-words features.

Class Precision Recall F1-Score Support
Critical 0.8534 0.8050 0.8285 200
High 0.7823 0.8000 0.7910 200
Low 0.7945 0.8750 0.8329 200
Medium 0.7812 0.7450 0.7627 200
Overall Accuracy 0.8063 800

Critical Clause Recall: 0.8050


Model 3 β€” XGBoost (Tuned)

Gradient boosting with hyperparameter tuning via RandomizedSearchCV.

Class Precision Recall F1-Score Support
Critical 1.0000 0.9050 0.9501 200
High 0.8955 0.9000 0.8978 200
Low 0.8609 0.9900 0.9209 200
Medium 0.8936 0.8400 0.8660 200
Overall Accuracy 0.9088 800

Critical Clause Recall: 0.9050


βœ… Final Model β€” RiskEnsembleClassifier (XGBoost + LightGBM + ExtraTrees)

Best performing model. Combines 3 models using soft voting (averages class probabilities).

Class Precision Recall F1-Score Support
Critical 1.0000 0.9000 0.9474 200
High 0.9069 0.9250 0.9158 200
Low 0.8795 0.9850 0.9292 200
Medium 0.8854 0.8500 0.8673 200
Overall Accuracy 0.9150 800

Critical Clause Recall: 0.9000


Model Comparison Summary

Model Accuracy Critical Recall Critical F1 Notes
Logistic Regression 72.25% 0.695 0.736 Baseline
Random Forest 80.63% 0.805 0.829 Better than baseline
XGBoost (Tuned) 90.88% 0.905 0.950 Big jump with tuning
RiskEnsembleClassifier 91.50% 0.900 0.947 Final model

Why XGBoost alone scored higher Critical Recall (0.905) vs Ensemble (0.900)? The ensemble slightly smooths out extreme predictions β€” it trades a tiny bit of critical recall for better overall balance across all 4 classes. The ensemble wins on overall accuracy (91.5% vs 90.88%).


Confusion Matrix (Final Model)

              Predicted
              Critical  High  Low  Medium
Actual  Critical   180    11    0       9
        High         0   185    5      10
        Low          0     0  197       3
        Medium       0     8   22     170

Key observations:

  • Critical β†’ only 9 misclassified as Medium, 11 as High. Zero misclassified as Low βœ“
  • Low β†’ nearly perfect (197/200 correct)
  • Medium β†’ 22 confused with Low (hardest class to distinguish)

Feature Engineering

Feature Type Method Count
Word n-grams TF-IDF (1,2)-grams, sublinear_tf 2000
Char n-grams TF-IDF (3,5)-grams, char_wb 1000
Numeric features modal_score, consequence_score, conditional_score, has_negation, obligation_count, penalty_flag, word_count 7
Total features 3007

Top 15 Important Features (XGBoost sub-model)

Feature Importance
ment (char) 0.0215
rmati (char) 0.0143
penalty_flag 0.0123
men (char) 0.0112
eri (char) 0.0112
acti (char) 0.0101
section agreement 0.0085
shall conduct 0.0075
consequence_score 0.0069
has_negation 0.0065

Difficulties Faced & How Overcome

1. Data Leakage in TF-IDF

Problem: Initially tfidf.fit_transform() was called on the full dataset before the train/test split. This meant the model had seen test-set vocabulary during training, giving artificially inflated metrics.

Solution: Restructured the pipeline β€” fit_transform only on training data, transform on test data. Accuracy dropped slightly but became trustworthy.


2. Memory Issue with Dense Matrices

Problem: Calling .toarray() on TF-IDF output converted sparse matrices to dense numpy arrays. With 3000+ features Γ— 4000 rows this consumed ~100MB+ RAM and slowed everything down.

Solution: Kept all matrices in sparse format (scipy.sparse.csr_matrix) throughout and used sp.hstack() for combining. Memory usage dropped by ~10x.


3. GridSearchCV Taking Too Long

Problem: Original GridSearchCV with 4Γ—3Γ—3Γ—3 = 108 combinations Γ— 3 folds = 324 full XGBoost training runs. On this dataset it would have taken hours.

Solution: Switched to RandomizedSearchCV with n_iter=20 β€” samples 20 random combinations instead of all 108. Runs in ~10 minutes with near-equivalent results.


4. Wrong Scoring Metric in Hyperparameter Search

Problem: GridSearchCV was using scoring='accuracy' but the most important metric for this project is Critical clause recall β€” a model that ignores the Critical class can still score high accuracy.

Solution: Created a custom scorer combining f1_macro + critical_recall (50/50 weight). This ensures hyperparameter search optimises for what actually matters.


5. Class Imbalance Handling

Problem: sample_weight was passed to RandomizedSearchCV causing a UserWarning about statistically incorrect results since the custom scorer doesn't support sample weights.

Solution: Moved class balancing inside each model using class_weight="balanced" parameter (LightGBM, ExtraTrees) instead of external sample weights. Warning eliminated.


6. IndentationError Crashing Script

Problem: A mid-file import statement had 4 spaces of accidental indentation, causing an IndentationError before the GridSearch even ran.

Solution: Moved all imports to the top of the file following PEP8 conventions.


Saved Model

The trained model is exported as a pickle bundle containing everything needed for inference:

import pickle, scipy.sparse as sp

with open("RiskEnsembleClassifier.pkl", "rb") as f:
    bundle = pickle.load(f)

# Predict on new clause
text  = ["employee must disclose all conflicts of interest immediately"]
word  = bundle["tfidf_word"].transform(text)
char  = bundle["tfidf_char"].transform(text)
num   = sp.csr_matrix(bundle["scaler"].transform([[0]*7]))
X_new = sp.hstack([word, char, num], format="csr")

pred  = bundle["model"].predict(X_new)
label = bundle["label_encoder"].inverse_transform(pred)
print(label)  # ['Critical']

Tech Stack

Python 3.13
scikit-learn    β€” TF-IDF, LabelEncoder, VotingClassifier, ExtraTrees, metrics
xgboost         β€” XGBClassifier
lightgbm        β€” LGBMClassifier
scipy           β€” sparse matrix operations
pandas          β€” data handling
numpy           β€” numerical ops
pickle          β€” model serialisation

Output Files

File Description
RiskEnsembleClassifier.pkl Saved model bundle (model + transformers + encoder)
ranked_clauses.csv All 4000 clauses ranked by composite risk score