CoolFace
Apppublic

ITNovaML/PCAgentinAI

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
agent10_severity.py234 linesDownload Raw Back to root
1"""2ClaimSense — Agent 10: Severity & Reserve Agent3=================================================4XGBoost + GLM ensemble for claim severity classification5and initial reserve estimation.6 7Training:  call train_severity_model()8Inference: call run_severity_agent(fnol_result, coverage_result, fraud_result, submission)9"""10 11import os, logging12import numpy as np13import pandas as pd14 15log = logging.getLogger(__name__)16MODELS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'models')17MODEL_PATH = os.path.join(MODELS_DIR, 'agent10_severity.pkl')18 19SEVERITY_BANDS = ['MINOR', 'MODERATE', 'MAJOR', 'CATASTROPHIC']20 21FEATURE_COLS = [22    'estimated_damage_norm', 'coverage_limit_norm', 'amount_ratio',23    'fraud_score', 'days_to_report', 'incident_type_encoded',24    'property_age', 'deductible_norm', 'has_police_report',25    'credit_score_norm', 'net_payable_norm',26]27 28 29def _encode_incident(t: str) -> int:30    return {'FIRE':4,'EARTHQUAKE':4,'FLOOD':4,'STRUCTURAL':3,31            'WIND':3,'HAIL':2,'WATER':2,'THEFT':2,32            'LIABILITY':1,'VANDALISM':1,'OTHER':1}.get(str(t).upper(), 1)33 34 35def _build_features(fnol: dict, coverage: dict, fraud: dict, submission: dict) -> dict:36    norm         = fnol.get('normalised_fields', {})37    estimated_dmg = float(norm.get('estimated_damage') or 0)38    coverage_limit= float(coverage.get('applicable_limit') or 300000)39    deductible    = float(coverage.get('deductible') or 2500)40    net_payable   = float(coverage.get('net_payable_est') or 0)41    fraud_score   = int(fraud.get('fraud_score') or 0)42    days_to_report= int(fnol.get('days_to_report') or 0)43    prop          = (submission.get('property') or {})44    yr_built      = int(prop.get('year_built') or 2000)45    prop_age      = max(0, 2026 - yr_built)46    credit_score  = float((submission.get('insured') or {}).get('credit_score') or 650)47    has_police    = int(bool((norm.get('has_police_report'))))48 49    return {50        'estimated_damage_norm': min(estimated_dmg / 500000, 3.0),51        'coverage_limit_norm':   coverage_limit / 1000000,52        'amount_ratio':          min(estimated_dmg / max(coverage_limit, 1), 3.0),53        'fraud_score':           fraud_score,54        'days_to_report':        days_to_report,55        'incident_type_encoded': _encode_incident(fnol.get('incident_type', 'OTHER')),56        'property_age':          min(prop_age, 100),57        'deductible_norm':       deductible / 50000,58        'has_police_report':     has_police,59        'credit_score_norm':     credit_score / 850,60        'net_payable_norm':      min(net_payable / 500000, 3.0),61    }62 63 64def _generate_synthetic_training(n=2500, seed=7):65    rng = np.random.default_rng(seed)66    rows = []67 68    for _ in range(n):69        coverage_limit = float(rng.choice([150000,250000,350000,500000,750000,1000000]))70        deductible     = float(rng.choice([1000,2500,5000,10000]))71        incident_enc   = int(rng.integers(1, 5))72        prop_age       = int(rng.integers(0, 80))73        fraud_sc       = int(rng.integers(0, 100))74        days_rep       = int(rng.integers(0, 120))75        credit_sc      = int(rng.integers(450, 820))76 77        # Base damage driven by incident severity78        base = {4: 0.65, 3: 0.35, 2: 0.18, 1: 0.08}.get(incident_enc, 0.1)79        estimated_dmg = float(coverage_limit * rng.uniform(base * 0.5, base * 1.5))80        estimated_dmg = min(estimated_dmg, coverage_limit * 1.1)81        net_payable   = max(0, min(estimated_dmg, coverage_limit) - deductible)82 83        # Severity band ground truth84        pct = estimated_dmg / coverage_limit85        if pct < 0.10:86            band = 'MINOR'87        elif pct < 0.35:88            band = 'MODERATE'89        elif pct < 0.70:90            band = 'MAJOR'91        else:92            band = 'CATASTROPHIC'93 94        # Final payout (slightly below estimate after adjustment)95        adj_factor  = rng.uniform(0.70, 0.95)96        final_payout = round(net_payable * adj_factor, 2)97 98        rows.append({99            'estimated_damage_norm': min(estimated_dmg / 500000, 3.0),100            'coverage_limit_norm':   coverage_limit / 1000000,101            'amount_ratio':          min(estimated_dmg / coverage_limit, 3.0),102            'fraud_score':           fraud_sc,103            'days_to_report':        days_rep,104            'incident_type_encoded': incident_enc,105            'property_age':          prop_age,106            'deductible_norm':       deductible / 50000,107            'has_police_report':     int(rng.random() < 0.6),108            'credit_score_norm':     credit_sc / 850,109            'net_payable_norm':      min(net_payable / 500000, 3.0),110            'severity_band':         band,111            'final_payout':          final_payout,112        })113 114    return pd.DataFrame(rows)115 116 117def train_severity_model():118    import joblib119    from xgboost import XGBClassifier, XGBRegressor120    from sklearn.model_selection import train_test_split121    from sklearn.preprocessing import LabelEncoder122    from sklearn.metrics import accuracy_score, mean_absolute_error123 124    os.makedirs(MODELS_DIR, exist_ok=True)125    log.info("[SEVERITY] Generating synthetic training data...")126    df = _generate_synthetic_training(n=3000)127 128    le = LabelEncoder()129    le.fit(SEVERITY_BANDS)130    df['severity_encoded'] = le.transform(df['severity_band'])131 132    X   = df[FEATURE_COLS]133    yc  = df['severity_encoded']134    yr  = df['final_payout']135 136    X_tr, X_te, yc_tr, yc_te, yr_tr, yr_te = train_test_split(137        X, yc, yr, test_size=0.2, random_state=42138    )139 140    clf = XGBClassifier(141        n_estimators=200, max_depth=5, learning_rate=0.07,142        subsample=0.85, colsample_bytree=0.85,143        eval_metric='mlogloss', random_state=42, verbosity=0144    )145    clf.fit(X_tr, yc_tr)146    acc = accuracy_score(yc_te, clf.predict(X_te))147    log.info(f"[SEVERITY] Classifier accuracy: {acc:.3f}")148 149    reg = XGBRegressor(150        n_estimators=200, max_depth=5, learning_rate=0.07,151        subsample=0.85, random_state=42, verbosity=0152    )153    reg.fit(X_tr, yr_tr)154    mae = mean_absolute_error(yr_te, reg.predict(X_te))155    log.info(f"[SEVERITY] Reserve regressor MAE: ${mae:,.0f}")156 157    model = {158        'classifier': clf, 'regressor': reg,159        'label_encoder': le, 'feature_cols': FEATURE_COLS,160        'accuracy': acc, 'mae': mae161    }162    joblib.dump(model, MODEL_PATH)163    log.info(f"[SEVERITY] Saved → {MODEL_PATH}")164    return model165 166 167def run_severity_agent(fnol_result: dict, coverage_result: dict,168                       fraud_result: dict, submission: dict) -> dict:169    claim_id = fnol_result.get('claim_id', '')170    log.info(f"[SEVERITY] Agent 10 running for {claim_id}")171 172    feats = _build_features(fnol_result, coverage_result, fraud_result, submission)173 174    severity_band   = 'MODERATE'175    reserve_estimate = 0.0176    method          = 'rules'177 178    try:179        import joblib180        model  = joblib.load(MODEL_PATH)181        X      = pd.DataFrame([feats])[model['feature_cols']]182        pred   = model['classifier'].predict(X)[0]183        severity_band    = model['label_encoder'].inverse_transform([pred])[0]184        reserve_estimate = float(np.clip(model['regressor'].predict(X)[0], 0, 5000000))185        method           = 'xgboost'186    except Exception as e:187        log.warning(f"[SEVERITY] ML unavailable: {e} — using rules")188        # Rules fallback189        ratio = feats['amount_ratio']190        if   ratio < 0.10: severity_band = 'MINOR'191        elif ratio < 0.35: severity_band = 'MODERATE'192        elif ratio < 0.70: severity_band = 'MAJOR'193        else:              severity_band = 'CATASTROPHIC'194 195        net = float(coverage_result.get('net_payable_est') or 0)196        reserve_estimate = net * 0.82197 198    # ── Reserve adjustment for fraud ─────────────────────────199    fraud_score = int(fraud_result.get('fraud_score') or 0)200    if fraud_score >= 70:201        reserve_estimate *= 0.5  # Hold lower reserve pending SIU202        reserve_note = f"Reserve reduced by 50% — high fraud score ({fraud_score})"203    elif fraud_score >= 40:204        reserve_estimate *= 0.75205        reserve_note = f"Reserve reduced by 25% — moderate fraud score ({fraud_score})"206    else:207        reserve_note = "Full reserve applied"208 209    reserve_estimate = round(reserve_estimate, 2)210 211    # ── Severity summary ──────────────────────────────────────212    severity_desc = {213        'MINOR':        'Low-value claim, likely straightforward settlement',214        'MODERATE':     'Standard claim requiring adjuster review',215        'MAJOR':        'Significant loss requiring senior adjuster and inspection',216        'CATASTROPHIC': 'Total or near-total loss — executive escalation required',217    }218 219    result = {220        'claim_id':         claim_id,221        'status':           'SEVERITY_SCORED',222        'severity_band':    severity_band,223        'severity_desc':    severity_desc.get(severity_band, ''),224        'reserve_estimate': reserve_estimate,225        'reserve_note':     reserve_note,226        '_method':          method,227    }228 229    log.info(230        f"[SEVERITY] {claim_id}: band={severity_band} "231        f"reserve=${reserve_estimate:,.0f} method={method}"232    )233    return result234