Spaces:
Sleeping
Sleeping
| """Phase 11: Reasoning engine. | |
| Builds analyst-facing structured reasoning from model prediction and | |
| explainability outputs. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| def _risk_level(risk_score: float) -> str: | |
| if risk_score >= 70: | |
| return "high" | |
| if risk_score >= 40: | |
| return "medium" | |
| return "low" | |
| def _recommended_action(classification: str, risk_score: float) -> str: | |
| if classification == "fraud" or risk_score >= 70: | |
| return "BLOCK" | |
| if classification == "suspicious" or risk_score >= 40: | |
| return "MFA" | |
| return "APPROVE" | |
| def _confidence( | |
| fraud_probability: float, | |
| fraud_threshold: float, | |
| suspicious_threshold: float, | |
| ) -> float: | |
| distance = max( | |
| abs(fraud_probability - fraud_threshold), | |
| abs(fraud_probability - suspicious_threshold), | |
| ) | |
| normalized = min(distance / max(1e-6, 1 - suspicious_threshold), 1.0) | |
| return round(0.55 + 0.44 * normalized, 4) | |
| def _top_categories(category_scores: dict[str, float], top_k: int = 3) -> list[dict[str, Any]]: | |
| ranked = sorted(category_scores.items(), key=lambda item: item[1], reverse=True) | |
| output: list[dict[str, Any]] = [] | |
| for name, score in ranked[:top_k]: | |
| output.append( | |
| { | |
| "category": name, | |
| "score": round(float(score), 6), | |
| "percent": round(float(score) * 100, 2), | |
| } | |
| ) | |
| return output | |
| def _signal_list(signals: list[dict[str, Any]], top_k: int = 3) -> list[dict[str, Any]]: | |
| output: list[dict[str, Any]] = [] | |
| for signal in signals[:top_k]: | |
| output.append( | |
| { | |
| "feature": signal.get("feature"), | |
| "label": signal.get("label"), | |
| "value": signal.get("value"), | |
| "impact": signal.get("direction"), | |
| "contribution": signal.get("shap_value"), | |
| } | |
| ) | |
| return output | |
| def _relationship_insight(category_scores: dict[str, float]) -> str: | |
| merchant = float(category_scores.get("merchant_risk", 0.0)) | |
| geo = float(category_scores.get("geo_anomaly", 0.0)) | |
| relationship_signal = merchant + geo | |
| if relationship_signal >= 0.4: | |
| return "Strong merchant and/or geo-cluster relationship risk signals are present." | |
| if relationship_signal >= 0.2: | |
| return "Moderate relationship-driven risk from merchant or geo clustering is observed." | |
| return "No strong relationship-risk concentration was detected." | |
| def _ato_insight(fraud_type: str, category_scores: dict[str, float]) -> str: | |
| ato_score = float(category_scores.get("account_takeover", 0.0)) | |
| if fraud_type == "account_takeover" or ato_score >= 0.45: | |
| return "ATO-style behavior is likely, driven by velocity and behavior-shift signals." | |
| if ato_score >= 0.2: | |
| return "Some ATO indicators are present but not dominant." | |
| return "ATO indicators are weak for this transaction." | |
| def _synthetic_identity_insight(fraud_type: str, category_scores: dict[str, float]) -> str: | |
| syn_score = float(category_scores.get("synthetic_identity", 0.0)) | |
| if fraud_type == "synthetic_identity" or syn_score >= 0.45: | |
| return "Synthetic-identity risk indicators are dominant for this transaction." | |
| if syn_score >= 0.2: | |
| return "Some synthetic-identity indicators are present but secondary." | |
| return "Synthetic-identity indicators are weak for this transaction." | |
| def _counterfactual_insight( | |
| positive_signals: list[dict[str, Any]], | |
| classification: str, | |
| ) -> str: | |
| if not positive_signals: | |
| return "No dominant positive risk driver was found for a stable counterfactual recommendation." | |
| top_driver = positive_signals[0] | |
| label = str(top_driver.get("label", top_driver.get("feature", "top signal"))).lower() | |
| if classification == "legitimate": | |
| return f"If {label} increases materially, this transaction may cross into suspicious risk." | |
| return f"If {label} were materially reduced, the score would likely move toward a lower-risk class." | |
| def build_reasoning_output( | |
| *, | |
| transaction_id: str | None, | |
| classification: str, | |
| fraud_probability: float, | |
| risk_score: float, | |
| fraud_threshold: float, | |
| suspicious_threshold: float, | |
| explainability: dict[str, Any], | |
| decision_action: str | None = None, | |
| ) -> dict[str, Any]: | |
| risk_level = _risk_level(risk_score) | |
| action = decision_action or _recommended_action(classification, risk_score) | |
| confidence = _confidence( | |
| fraud_probability=fraud_probability, | |
| fraud_threshold=fraud_threshold, | |
| suspicious_threshold=suspicious_threshold, | |
| ) | |
| category_scores = explainability.get("category_scores", {}) | |
| positive_signals = explainability.get("top_positive_signals", []) | |
| negative_signals = explainability.get("top_negative_signals", []) | |
| fraud_type = str(explainability.get("fraud_type", "general_fraud")) | |
| headline = f"{classification.upper()} decision with {risk_level.upper()} risk" | |
| summary = str(explainability.get("explanation_summary", "No explanation summary available.")) | |
| what_changed = "Risk increased due to strong positive driver signals." | |
| if classification == "legitimate": | |
| what_changed = "Risk remained contained due to stronger counter-signals than risk drivers." | |
| return { | |
| "transaction_id": transaction_id, | |
| "headline": headline, | |
| "risk_level": risk_level, | |
| "risk_breakdown": _top_categories(category_scores), | |
| "summary": summary, | |
| "what_changed": what_changed, | |
| "key_signals": _signal_list(positive_signals), | |
| "counter_signals": _signal_list(negative_signals), | |
| "relationship_insight": _relationship_insight(category_scores), | |
| "ato_insight": _ato_insight(fraud_type, category_scores), | |
| "synthetic_identity_insight": _synthetic_identity_insight(fraud_type, category_scores), | |
| "counterfactual_insight": _counterfactual_insight(positive_signals, classification), | |
| "recommended_action": action, | |
| "confidence": confidence, | |
| "classification": classification, | |
| "fraud_probability": round(float(fraud_probability), 6), | |
| "risk_score": round(float(risk_score), 2), | |
| "fraud_type": fraud_type, | |
| } | |
| def phase_status() -> str: | |
| return "phase 11 reasoning engine implemented" | |