File size: 8,889 Bytes
993fce6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# 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

```bash
# 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:

```python
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 |