Spaces:
Runtime error
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 |