#!/usr/bin/env python3 """app.py — App de inferencia para detección de fraude en tarjetas de crédito. Descarga el modelo XGBoost desde HF Hub y expone una interfaz Gradio para predecir si una transacción es fraudulenta. Desplegada en Hugging Face Spaces. """ import gradio as gr import joblib import numpy as np import os from huggingface_hub import hf_hub_download # --- Configuración --- MODEL_REPO = os.environ.get("HF_MODEL_REPO", "gusdelact/credit-card-fraud-xgboost") OPTIMAL_THRESHOLD = 0.9306 # Calibrado con curva precision-recall # Categorías de transacción disponibles CATEGORIES = [ "entertainment", "food_dining", "gas_transport", "grocery_net", "grocery_pos", "health_fitness", "home", "kids_pets", "misc_net", "misc_pos", "personal_care", "shopping_net", "shopping_pos", "travel" ] def load_model(): """Descarga y carga el modelo desde HF Hub.""" print(f"Descargando modelo desde: {MODEL_REPO}") model_path = hf_hub_download(MODEL_REPO, "model.joblib") model = joblib.load(model_path) print("✅ Modelo cargado correctamente") return model # Cargar modelo al iniciar model = load_model() def build_feature_vector(amt, lat, long, city_pop, merch_lat, merch_long, merch_zipcode, category, gender): """Construye el vector de features en el orden esperado por el modelo. Features (23 total): - 7 numéricas (estandarizadas): amt, lat, long, city_pop, merch_lat, merch_long, merch_zipcode - 14 one-hot category: entertainment, food_dining, gas_transport, grocery_net, grocery_pos, health_fitness, home, kids_pets, misc_net, misc_pos, personal_care, shopping_net, shopping_pos, travel - 2 one-hot gender: F, M """ # Numéricas (nota: en producción deberían pasar por el StandardScaler, # pero para la demo usamos valores directos ya que el modelo fue entrenado # con datos escalados) features = [amt, lat, long, city_pop, merch_lat, merch_long, merch_zipcode] # One-hot encoding de categoría for cat in CATEGORIES: features.append(1.0 if category == cat else 0.0) # One-hot encoding de género features.append(1.0 if gender == "F" else 0.0) features.append(1.0 if gender == "M" else 0.0) return np.array(features).reshape(1, -1) def predict_fraud(amt, lat, long, city_pop, merch_lat, merch_long, merch_zipcode, category, gender, threshold): """Predice si una transacción es fraudulenta.""" try: X = build_feature_vector( amt, lat, long, city_pop, merch_lat, merch_long, merch_zipcode, category, gender ) proba = model.predict_proba(X)[0, 1] is_fraud = proba >= threshold # Resultado formateado result = { "🚨 FRAUDE": float(proba), "✅ Legítima": float(1 - proba), } detail = f"**Probabilidad de fraude: {proba:.4f}**\n\n" detail += f"Umbral aplicado: {threshold:.4f}\n\n" if is_fraud: detail += "⚠️ **ALERTA: Transacción clasificada como FRAUDULENTA**\n\n" detail += f"La probabilidad ({proba:.4f}) supera el umbral ({threshold:.4f})." else: detail += "✅ **Transacción clasificada como LEGÍTIMA**\n\n" detail += f"La probabilidad ({proba:.4f}) está por debajo del umbral ({threshold:.4f})." return result, detail except Exception as e: return {"Error": 1.0}, f"❌ Error: {str(e)}" # --- Ejemplos predefinidos --- examples = [ # Transacción legítima típica (monto bajo, gasolina) [25.50, 38.0, -90.0, 50000, 38.1, -90.1, 63101, "gas_transport", "M", OPTIMAL_THRESHOLD], # Transacción sospechosa (monto alto, entretenimiento) [950.00, 40.7, -74.0, 8000000, 41.0, -73.5, 10001, "entertainment", "F", OPTIMAL_THRESHOLD], # Compra online moderada [150.00, 34.0, -118.2, 3900000, 34.1, -118.0, 90001, "shopping_net", "F", OPTIMAL_THRESHOLD], # Transacción alta en viajes [1200.00, 25.8, -80.2, 400000, 26.0, -80.0, 33101, "travel", "M", OPTIMAL_THRESHOLD], ] # --- UI --- with gr.Blocks(theme=gr.themes.Soft(), title="🔍 Fraud Detector") as demo: gr.Markdown(""" # 🔍 Credit Card Fraud Detector Modelo XGBoost entrenado con SMOTE para detectar transacciones fraudulentas. **Dataset**: [alenc123/credit-card-fraud](https://huggingface.co/datasets/alenc123/credit-card-fraud) | **Modelo**: [gusdelact/credit-card-fraud-xgboost](https://huggingface.co/gusdelact/credit-card-fraud-xgboost) """) with gr.Tab("🔮 Predicción"): with gr.Row(): with gr.Column(scale=2): gr.Markdown("### Datos de la Transacción") amt = gr.Number(label="Monto (USD)", value=100.0, minimum=1.0) with gr.Row(): category = gr.Dropdown( choices=CATEGORIES, value="grocery_pos", label="Categoría" ) gender = gr.Radio(choices=["M", "F"], value="M", label="Género") gr.Markdown("### Ubicación del Titular") with gr.Row(): lat = gr.Number(label="Latitud", value=38.0) long = gr.Number(label="Longitud", value=-90.0) city_pop = gr.Number(label="Población ciudad", value=50000) gr.Markdown("### Ubicación del Comercio") with gr.Row(): merch_lat = gr.Number(label="Latitud comercio", value=38.1) merch_long = gr.Number(label="Longitud comercio", value=-90.1) merch_zipcode = gr.Number(label="ZIP comercio", value=63101) threshold = gr.Slider( 0.1, 0.99, value=OPTIMAL_THRESHOLD, step=0.01, label="Umbral de decisión", info="Calibrado en 0.93 para maximizar F1. Bajar para más sensibilidad." ) predict_btn = gr.Button("🔍 Analizar Transacción", variant="primary", size="lg") with gr.Column(scale=1): gr.Markdown("### Resultado") output_label = gr.Label(num_top_classes=2, label="Clasificación") output_detail = gr.Markdown(label="Detalle") predict_btn.click( fn=predict_fraud, inputs=[amt, lat, long, city_pop, merch_lat, merch_long, merch_zipcode, category, gender, threshold], outputs=[output_label, output_detail], ) gr.Examples( examples=examples, inputs=[amt, lat, long, city_pop, merch_lat, merch_long, merch_zipcode, category, gender, threshold], outputs=[output_label, output_detail], fn=predict_fraud, cache_examples=False, ) with gr.Tab("📊 Info del Modelo"): gr.Markdown(f""" ## Información del Modelo | Aspecto | Detalle | |---------|---------| | **Tipo** | XGBClassifier | | **Técnica de balanceo** | SMOTE (sampling_strategy=0.2) | | **Features** | 23 (7 numéricas + 16 one-hot) | | **ROC-AUC** | 0.9940 | | **PR-AUC** | 0.7878 | | **F1 (umbral calibrado)** | 0.7435 | | **Umbral óptimo** | 0.9306 | ### Hiperparámetros (RandomizedSearchCV, CV=5) | Parámetro | Valor | |-----------|-------| | max_depth | 6 | | n_estimators | 250 | | learning_rate | 0.1 | | subsample | 0.8 | | colsample_bytree | 0.6 | | reg_alpha | 0.1 | | reg_lambda | 1 | | min_child_weight | 3 | | tree_method | hist | ### Sobre el Umbral El umbral por defecto (0.5) da un recall alto (85%) pero muchos falsos positivos. El umbral calibrado (0.93) maximiza el F1-score, balanceando precision y recall. - **Umbral bajo** → más transacciones marcadas como fraude (más seguro, más falsos positivos) - **Umbral alto** → solo marca fraudes muy claros (menos alertas, puede dejar pasar fraudes) ### Dataset [alenc123/credit-card-fraud](https://huggingface.co/datasets/alenc123/credit-card-fraud) — 1.3M transacciones simuladas con 0.58% de fraude. """) if __name__ == "__main__": demo.launch()