fraud_detection_model / reasoning.py
Samyak000's picture
Upload 2 files
d88df03 verified
Raw
History Blame Contribute Delete
6.55 kB
"""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"