Spaces:
Runtime error
Runtime error
| """Clause Risk Classifier — Testing Script | |
| ======================================== | |
| Step 1: Run this ONCE to save the trained model to disk | |
| Step 2: Use the saved model to: | |
| (A) Predict on a new CSV file | |
| (B) Type a single clause and get a prediction instantly | |
| (C) Error analysis — see exactly where the model is wrong | |
| """ | |
| import os | |
| import sys | |
| import pickle | |
| import argparse | |
| import warnings | |
| warnings.filterwarnings("ignore", category=UserWarning) | |
| import numpy as np | |
| import pandas as pd | |
| import scipy.sparse as sp | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from sklearn.preprocessing import LabelEncoder, StandardScaler | |
| from sklearn.metrics import classification_report, confusion_matrix, recall_score | |
| # ============================================================ | |
| # STEP 1 — SAVE YOUR TRAINED MODEL | |
| # Run: python test_model.py --mode save --data "path/to/master_dataset.csv" | |
| # This trains the model and saves it to model_bundle.pkl | |
| # ============================================================ | |
| def save_model(data_path): | |
| """Train on full data and save everything needed for prediction.""" | |
| from sklearn.ensemble import VotingClassifier, ExtraTreesClassifier | |
| import xgboost as xgb | |
| import lightgbm as lgb | |
| from sklearn.model_selection import train_test_split | |
| print("Loading data...") | |
| df = pd.read_csv(data_path) | |
| num_cols = [ | |
| "modal_score", "consequence_score", "conditional_score", | |
| "has_negation", "obligation_count", "penalty_flag", "word_count", | |
| ] | |
| le = LabelEncoder() | |
| y = le.fit_transform(df["risk_label"]) | |
| # Fit transformers on FULL dataset (for production use) | |
| tfidf_word = TfidfVectorizer(max_features=2000, stop_words="english", | |
| ngram_range=(1, 2), sublinear_tf=True) | |
| tfidf_char = TfidfVectorizer(max_features=1000, analyzer="char_wb", | |
| ngram_range=(3, 5), sublinear_tf=True) | |
| scaler = StandardScaler() | |
| text_word = tfidf_word.fit_transform(df["clean_text"].fillna("")) | |
| text_char = tfidf_char.fit_transform(df["clean_text"].fillna("")) | |
| num = sp.csr_matrix(scaler.fit_transform(df[num_cols].fillna(0).values)) | |
| X = sp.hstack([text_word, text_char, num], format="csr") | |
| # Best params from v3 tuning | |
| 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, class_weight="balanced", | |
| random_state=42, verbose=-1, | |
| ) | |
| et_model = ExtraTreesClassifier( | |
| n_estimators=300, min_samples_leaf=2, | |
| class_weight="balanced", random_state=42, n_jobs=-1, | |
| ) | |
| model = VotingClassifier( | |
| estimators=[("xgb", xgb_model), ("lgb", lgb_model), ("et", et_model)], | |
| voting="soft", n_jobs=1, | |
| ) | |
| print("Training model on full dataset...") | |
| model.fit(X, y) | |
| # Bundle everything needed for inference | |
| bundle = { | |
| "model": model, | |
| "tfidf_word": tfidf_word, | |
| "tfidf_char": tfidf_char, | |
| "scaler": scaler, | |
| "le": le, | |
| "num_cols": num_cols, | |
| } | |
| with open("model_bundle.pkl", "wb") as f: | |
| pickle.dump(bundle, f) | |
| print("\nModel saved to model_bundle.pkl") | |
| print(f"Classes: {list(le.classes_)}") | |
| # ============================================================ | |
| # SHARED HELPER — load bundle + transform raw input | |
| # ============================================================ | |
| def load_bundle(path="model_bundle.pkl"): | |
| if not os.path.exists(path): | |
| print("ERROR: model_bundle.pkl not found.") | |
| print("Run this first: python test_model.py --mode save --data your_data.csv") | |
| sys.exit(1) | |
| with open(path, "rb") as f: | |
| return pickle.load(f) | |
| def transform(bundle, texts, num_array): | |
| """Apply saved transformers to raw inputs.""" | |
| word = bundle["tfidf_word"].transform(texts) | |
| char = bundle["tfidf_char"].transform(texts) | |
| num = sp.csr_matrix(bundle["scaler"].transform(num_array)) | |
| return sp.hstack([word, char, num], format="csr") | |
| # ============================================================ | |
| # MODE A — Predict on a new CSV file | |
| # Run: python test_model.py --mode csv --data "path/to/new_file.csv" | |
| # The CSV must have the same columns as the training data. | |
| # If it has a risk_label column, a full evaluation report is shown. | |
| # If not, predictions are just saved to predictions.csv. | |
| # ============================================================ | |
| def test_on_csv(data_path): | |
| bundle = load_bundle() | |
| le = bundle["le"] | |
| nc = bundle["num_cols"] | |
| print(f"Loading: {data_path}") | |
| df = pd.read_csv(data_path) | |
| df["clean_text"] = df["clean_text"].fillna("") | |
| X = transform(bundle, df["clean_text"], df[nc].fillna(0).values) | |
| df["predicted_risk_label"] = le.inverse_transform(bundle["model"].predict(X)) | |
| # Confidence scores (probability of the winning class) | |
| probs = bundle["model"].predict_proba(X) | |
| df["confidence"] = (np.max(probs, axis=1) * 100).round(1).astype(str) + "%" | |
| print("\n=== Sample Predictions (first 10) ===") | |
| cols = ["clause_id", "clean_text", "predicted_risk_label", "confidence"] | |
| if "risk_label" in df.columns: | |
| cols.insert(3, "risk_label") | |
| print(df.head(10)[cols].to_string(index=False)) | |
| # If ground-truth labels exist → full evaluation | |
| if "risk_label" in df.columns: | |
| y_true = le.transform(df["risk_label"]) | |
| y_pred = le.transform(df["predicted_risk_label"]) | |
| critical_idx = list(le.classes_).index("Critical") | |
| print("\n=== Classification Report ===") | |
| print(classification_report(df["risk_label"], df["predicted_risk_label"], digits=4)) | |
| per_class = recall_score(y_true, y_pred, average=None) | |
| print(f"Critical Clause Recall: {per_class[critical_idx]:.4f}") | |
| print("\nConfusion Matrix:") | |
| cm = confusion_matrix(y_true, y_pred) | |
| print(pd.DataFrame(cm, index=le.classes_, columns=le.classes_)) | |
| out_path = "predictions.csv" | |
| df.to_csv(out_path, index=False) | |
| print(f"\nSaved all predictions to {out_path}") | |
| # ============================================================ | |
| # MODE B — Interactive: type a clause, get a prediction | |
| # Run: python test_model.py --mode interactive | |
| # Numeric features default to 0 — you can edit them in the prompt. | |
| # ============================================================ | |
| def interactive_mode(): | |
| bundle = load_bundle() | |
| le = bundle["le"] | |
| nc = bundle["num_cols"] | |
| severity_emoji = {"Critical": "CRITICAL", "High": "HIGH", | |
| "Medium": "MEDIUM", "Low": "LOW"} | |
| print("\n=== Interactive Clause Tester ===") | |
| print("Type a clause and press Enter. Type 'quit' to exit.\n") | |
| while True: | |
| clause = input("Clause text: ").strip() | |
| if clause.lower() in ("quit", "exit", "q"): | |
| break | |
| if not clause: | |
| continue | |
| # Optional: ask for numeric features | |
| print("Numeric features (press Enter to use 0 for all):") | |
| num_vals = [] | |
| for col in nc: | |
| val = input(f" {col} [0]: ").strip() | |
| try: | |
| num_vals.append(float(val) if val else 0.0) | |
| except ValueError: | |
| num_vals.append(0.0) | |
| X = transform(bundle, pd.Series([clause]), | |
| np.array([num_vals])) | |
| pred = bundle["model"].predict(X)[0] | |
| probs = bundle["model"].predict_proba(X)[0] | |
| label = le.inverse_transform([pred])[0] | |
| print(f"\n Prediction : [{severity_emoji[label]}] {label}") | |
| print(" Confidence breakdown:") | |
| for i, cls in enumerate(le.classes_): | |
| bar = "█" * int(probs[i] * 20) | |
| print(f" {cls:<10s} {probs[i]*100:5.1f}% {bar}") | |
| print() | |
| # ============================================================ | |
| # MODE C — Error analysis | |
| # Run: python test_model.py --mode errors --data "path/to/data.csv" | |
| # Shows every clause the model got wrong, sorted by confidence. | |
| # Helps you understand failure patterns. | |
| # ============================================================ | |
| def error_analysis(data_path): | |
| bundle = load_bundle() | |
| le = bundle["le"] | |
| nc = bundle["num_cols"] | |
| df = pd.read_csv(data_path) | |
| if "risk_label" not in df.columns: | |
| print("ERROR: CSV must have a 'risk_label' column for error analysis.") | |
| sys.exit(1) | |
| df["clean_text"] = df["clean_text"].fillna("") | |
| X = transform(bundle, df["clean_text"], df[nc].fillna(0).values) | |
| y_true = le.transform(df["risk_label"]) | |
| y_pred = bundle["model"].predict(X) | |
| probs = bundle["model"].predict_proba(X) | |
| df["predicted_risk_label"] = le.inverse_transform(y_pred) | |
| df["confidence"] = np.max(probs, axis=1).round(3) | |
| df["correct"] = df["risk_label"] == df["predicted_risk_label"] | |
| errors = df[~df["correct"]].copy() | |
| errors = errors.sort_values("confidence", ascending=False) | |
| total = len(df) | |
| n_errors = len(errors) | |
| print(f"\nTotal clauses : {total}") | |
| print(f"Correct : {total - n_errors} ({(total-n_errors)/total*100:.1f}%)") | |
| print(f"Wrong : {n_errors} ({n_errors/total*100:.1f}%)") | |
| print("\n=== Most Confident Mistakes (model was very sure but wrong) ===") | |
| top_errors = errors.head(20)[ | |
| ["clause_id", "clean_text", "risk_label", | |
| "predicted_risk_label", "confidence"] | |
| ] | |
| pd.set_option("display.max_colwidth", 60) | |
| print(top_errors.to_string(index=False)) | |
| print("\n=== Confusion breakdown (what gets confused with what) ===") | |
| for true_cls in le.classes_: | |
| subset = errors[errors["risk_label"] == true_cls] | |
| if len(subset) == 0: | |
| continue | |
| counts = subset["predicted_risk_label"].value_counts() | |
| print(f" True={true_cls:<10s} predicted as: " | |
| + ", ".join(f"{k}({v})" for k, v in counts.items())) | |
| errors.to_csv("errors.csv", index=False) | |
| print("\nAll errors saved to errors.csv") | |
| # ============================================================ | |
| # MAIN | |
| # ============================================================ | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Test the clause risk classifier") | |
| parser.add_argument("--mode", required=True, | |
| choices=["save", "csv", "interactive", "errors"], | |
| help="save | csv | interactive | errors") | |
| parser.add_argument("--data", | |
| default=r"C:\Users\Satya\Downloads\master_dataset.csv", | |
| help="Path to CSV (needed for save / csv / errors modes)") | |
| args = parser.parse_args() | |
| if args.mode == "save": | |
| data_path = args.data.strip().strip("\"'") | |
| if not os.path.exists(data_path): | |
| print(f"ERROR: File not found: {data_path}") | |
| sys.exit(1) | |
| save_model(data_path) | |
| elif args.mode == "csv": | |
| data_path = args.data.strip().strip("\"'") | |
| if not os.path.exists(data_path): | |
| print(f"ERROR: File not found: {data_path}") | |
| sys.exit(1) | |
| test_on_csv(data_path) | |
| elif args.mode == "interactive": | |
| interactive_mode() | |
| elif args.mode == "errors": | |
| data_path = args.data.strip().strip("\"'") | |
| if not os.path.exists(data_path): | |
| print(f"ERROR: File not found: {data_path}") | |
| sys.exit(1) | |
| error_analysis(data_path) | |