| import os |
| import uuid |
| import pandas as pd |
| import numpy as np |
| from fastapi import FastAPI, UploadFile, File, Form, HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.responses import FileResponse, StreamingResponse |
| from pydantic import BaseModel, Field |
| from typing import Dict, Any, List, Optional |
| import io |
| import json |
| import sys |
|
|
| |
| sys.path.append(os.path.dirname(os.path.abspath(__file__))) |
|
|
| from predictor import get_predictor |
|
|
| app = FastAPI( |
| title="Suspicious Transaction & Mule Account Detector API", |
| description="Backend API running ML predictions to identify suspicious transactions and mule accounts.", |
| version="1.0.0" |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=False, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| def clean_nans(data): |
| if isinstance(data, dict): |
| return {k: clean_nans(v) for k, v in data.items()} |
| elif isinstance(data, list): |
| return [clean_nans(v) for v in data] |
| elif isinstance(data, float): |
| if np.isnan(data) or np.isinf(data): |
| return None |
| return data |
| elif pd.isna(data): |
| return None |
| else: |
| return data |
|
|
| |
| RUNS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "runs")) |
| os.makedirs(RUNS_DIR, exist_ok=True) |
|
|
| |
| |
| class SingleTransactionRequest(BaseModel): |
| data: Dict[str, Any] |
| model_name: Optional[str] = "voting" |
| threshold: Optional[float] = 0.30 |
|
|
| class SingleTransactionResponse(BaseModel): |
| probability: float |
| is_suspicious: bool |
| risk_level: str |
|
|
| @app.get("/api/health") |
| def health_check(): |
| try: |
| get_predictor() |
| return {"status": "healthy", "model_loaded": True} |
| except Exception as e: |
| return {"status": "degraded", "error": str(e), "model_loaded": False} |
|
|
| @app.get("/api/model-info") |
| def model_info(): |
| predictor = get_predictor() |
| return { |
| "cat_cols": predictor.cat_cols, |
| "features_count": len(predictor.ohe_columns), |
| "available_models": list(predictor.models.keys()), |
| "default_thresholds": { |
| "voting": 0.30, |
| "xgboost": 0.30, |
| "random_forest": 0.28 |
| } |
| } |
|
|
| @app.post("/api/predict-single", response_model=SingleTransactionResponse) |
| def predict_single(req: SingleTransactionRequest): |
| try: |
| predictor = get_predictor() |
| res = predictor.predict_transaction( |
| data_row=req.data, |
| model_name=req.model_name, |
| threshold=req.threshold |
| ) |
| return res |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}") |
|
|
| @app.post("/api/predict-csv") |
| async def predict_csv( |
| file: UploadFile = File(...), |
| model_name: str = Form("voting"), |
| threshold: float = Form(0.30) |
| ): |
| |
| if not file.filename.endswith('.csv'): |
| raise HTTPException(status_code=400, detail="Only CSV files are supported.") |
| |
| try: |
| |
| contents = await file.read() |
| df = pd.read_csv(io.BytesIO(contents)) |
| |
| if len(df) == 0: |
| raise HTTPException(status_code=400, detail="The uploaded CSV file is empty.") |
| |
| predictor = get_predictor() |
| |
| |
| results = predictor.predict_dataframe(df, model_name=model_name, threshold=threshold) |
| |
| |
| df_annotated = df.copy() |
| df_annotated['fraud_probability'] = results['probability'] |
| df_annotated['is_suspicious'] = results['is_suspicious'] |
| df_annotated['risk_level'] = results['risk_level'] |
| |
| |
| session_id = str(uuid.uuid4()) |
| run_file_path = os.path.join(RUNS_DIR, f"{session_id}.csv") |
| df_annotated.to_csv(run_file_path, index=False) |
| |
| |
| total_count = len(df_annotated) |
| suspicious_count = int(df_annotated['is_suspicious'].sum()) |
| suspicious_rate = float(df_annotated['is_suspicious'].mean()) |
| |
| |
| risk_counts = df_annotated['risk_level'].value_counts().to_dict() |
| risk_distribution = { |
| "High": int(risk_counts.get("High", 0)), |
| "Medium": int(risk_counts.get("Medium", 0)), |
| "Low": int(risk_counts.get("Low", 0)) |
| } |
| |
| |
| cat_analyses = {} |
| for col in ['F3886', 'F3893', 'F3891', 'F3892']: |
| if col in df_annotated.columns: |
| |
| grouped = df_annotated.groupby(col).agg( |
| total=('fraud_probability', 'count'), |
| suspicious=('is_suspicious', 'sum'), |
| avg_prob=('fraud_probability', 'mean') |
| ).reset_index() |
| |
| grouped = grouped.sort_values(by='suspicious', ascending=False) |
| |
| cat_analyses[col] = grouped.to_dict(orient='records') |
| |
| |
| time_series = [] |
| if 'F3888' in df_annotated.columns: |
| |
| dates = pd.to_datetime(df_annotated['F3888'], errors='coerce') |
| df_temp = df_annotated.copy() |
| df_temp['date_str'] = dates.dt.strftime('%Y-%m-%d') |
| |
| df_temp = df_temp.dropna(subset=['date_str']) |
| if len(df_temp) > 0: |
| ts_grouped = df_temp.groupby('date_str').agg( |
| total=('fraud_probability', 'count'), |
| suspicious=('is_suspicious', 'sum') |
| ).reset_index() |
| |
| ts_grouped = ts_grouped.sort_values(by='date_str') |
| time_series = ts_grouped.to_dict(orient='records') |
|
|
| |
| |
| display_cols = ['Unnamed: 0', 'F3888', 'F3886', 'F3893', 'F3891', 'F3892', 'F3887', 'F3894', 'F3895'] |
| existing_display_cols = [c for c in display_cols if c in df_annotated.columns] |
| extra_cols = ['fraud_probability', 'is_suspicious', 'risk_level'] |
| |
| preview_df = df_annotated[existing_display_cols + extra_cols].head(100) |
| |
| preview_df = preview_df.replace({np.nan: None}) |
| preview_records = preview_df.to_dict(orient='records') |
| |
| |
| top_suspicious_df = df_annotated[existing_display_cols + extra_cols].sort_values(by='fraud_probability', ascending=False).head(50) |
| top_suspicious_df = top_suspicious_df.replace({np.nan: None}) |
| top_suspicious_records = top_suspicious_df.to_dict(orient='records') |
|
|
| |
| |
| mule_accounts = [] |
| acct_col = 'F3887' if 'F3887' in df_annotated.columns else ('F3895' if 'F3895' in df_annotated.columns else None) |
| if acct_col: |
| mule_grouped = df_annotated.groupby(acct_col).agg( |
| tx_count=('fraud_probability', 'count'), |
| suspicious_count=('is_suspicious', 'sum'), |
| max_prob=('fraud_probability', 'max'), |
| avg_prob=('fraud_probability', 'mean') |
| ).reset_index() |
| |
| mule_candidates = mule_grouped[mule_grouped['suspicious_count'] > 0].sort_values(by=['suspicious_count', 'avg_prob'], ascending=False).head(20) |
| mule_candidates = mule_candidates.replace({np.nan: None}) |
| mule_accounts = mule_candidates.to_dict(orient='records') |
|
|
| return clean_nans({ |
| "session_id": session_id, |
| "total_count": total_count, |
| "suspicious_count": suspicious_count, |
| "suspicious_rate": round(suspicious_rate, 4), |
| "risk_distribution": risk_distribution, |
| "categorical_analyses": cat_analyses, |
| "time_series": time_series, |
| "preview": preview_records, |
| "top_suspicious": top_suspicious_records, |
| "mule_accounts": mule_accounts |
| }) |
| |
| except Exception as e: |
| import traceback |
| traceback.print_exc() |
| raise HTTPException(status_code=500, detail=f"CSV processing failed: {str(e)}") |
|
|
| @app.get("/api/download-run/{session_id}") |
| def download_run(session_id: str): |
| file_path = os.path.join(RUNS_DIR, f"{session_id}.csv") |
| if not os.path.exists(file_path): |
| raise HTTPException(status_code=404, detail="Processed file not found. Session may have expired.") |
| return FileResponse( |
| path=file_path, |
| media_type="text/csv", |
| filename=f"suspicious_detection_report_{session_id[:8]}.csv" |
| ) |
|
|
| |
| from fastapi.staticfiles import StaticFiles |
| static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "static")) |
| if os.path.exists(static_dir): |
| app.mount("/", StaticFiles(directory=static_dir, html=True), name="static") |
|
|
|
|