Commit ·
6dfed49
1
Parent(s): b55491c
refactor: modulariza arquitetura do backend e frontend
Browse files- backend: separa lógica de machine learning em services, rotas em api e configs em core
- frontend: isola requisições HTTP em api_client e componentes visuais de formulário em forms
- .gitignore +3 -0
- app/api/__init__.py +1 -0
- app/api/routes.py +19 -0
- app/core/__init__.py +1 -0
- app/core/config.py +13 -0
- app/main.py +3 -44
- app/services/__init__.py +1 -0
- app/services/prediction.py +42 -0
- ui/components/__init__.py +1 -0
- ui/components/forms.py +32 -0
- ui/services/__init__.py +1 -0
- ui/services/api_client.py +18 -0
- ui/ui.py +13 -39
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
app/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Inicializa a camada de api
|
app/api/routes.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from app.schemas import CustomerData
|
| 3 |
+
from app.services.prediction import prediction_service
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
@router.get("/")
|
| 8 |
+
def home():
|
| 9 |
+
return {"status": "online", "module": "production_api"}
|
| 10 |
+
|
| 11 |
+
@router.post("/predict")
|
| 12 |
+
def predict_cluster(customer: CustomerData):
|
| 13 |
+
# Converte o Pydantic model para dict
|
| 14 |
+
data_dict = customer.model_dump()
|
| 15 |
+
|
| 16 |
+
# Executa a predição via serviço
|
| 17 |
+
result = prediction_service.predict(data_dict)
|
| 18 |
+
|
| 19 |
+
return result
|
app/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Inicializa o módulo core
|
app/core/config.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
class Settings:
|
| 4 |
+
# Obtendo o diretório raiz do projeto (subindo de app/core/config.py para a raiz)
|
| 5 |
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 6 |
+
MODELS_DIR = os.path.join(BASE_DIR, "models")
|
| 7 |
+
|
| 8 |
+
# Adicionando nomes dos arquivos de modelo para ficar centralizado
|
| 9 |
+
SCALER_PATH = os.path.join(MODELS_DIR, 'standard_scaler.joblib')
|
| 10 |
+
KMEANS_PATH = os.path.join(MODELS_DIR, 'kmeans_model.joblib')
|
| 11 |
+
RF_PATH = os.path.join(MODELS_DIR, 'random_forest_model.joblib')
|
| 12 |
+
|
| 13 |
+
settings = Settings()
|
app/main.py
CHANGED
|
@@ -1,48 +1,7 @@
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
-
import
|
| 3 |
-
import pandas as pd
|
| 4 |
-
import os
|
| 5 |
-
from .schemas import CustomerData
|
| 6 |
-
from .personas import PERSONAS
|
| 7 |
-
|
| 8 |
-
# Caminho relativo para os modelos
|
| 9 |
-
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 10 |
-
MODELS_DIR = os.path.join(BASE_DIR, "models")
|
| 11 |
-
|
| 12 |
-
scaler = joblib.load(os.path.join(MODELS_DIR, 'standard_scaler.joblib'))
|
| 13 |
-
kmeans = joblib.load(os.path.join(MODELS_DIR, 'kmeans_model.joblib'))
|
| 14 |
-
rf_model = joblib.load(os.path.join(MODELS_DIR, 'random_forest_model.joblib'))
|
| 15 |
|
| 16 |
app = FastAPI(title="Credit Card Clustering API")
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
return {"status": "online", "module": "production_api"}
|
| 21 |
-
|
| 22 |
-
@app.post("/predict")
|
| 23 |
-
def predict_cluster(customer: CustomerData):
|
| 24 |
-
data_dict = customer.model_dump()
|
| 25 |
-
df_input = pd.DataFrame([data_dict])
|
| 26 |
-
|
| 27 |
-
# Reordenar as colunas para o exato padrão que o Scikit-Learn espera no modelo treinado
|
| 28 |
-
expected_features = [
|
| 29 |
-
'BALANCE_FREQUENCY', 'PURCHASES', 'CASH_ADVANCE',
|
| 30 |
-
'ONEOFF_PURCHASES_FREQUENCY', 'CREDIT_LIMIT', 'PRC_FULL_PAYMENT',
|
| 31 |
-
'TENURE', 'CREDIT_UTILIZATION', 'PAYMENT_RATIO'
|
| 32 |
-
]
|
| 33 |
-
df_input = df_input[expected_features]
|
| 34 |
-
|
| 35 |
-
scaled_data = scaler.transform(df_input)
|
| 36 |
-
|
| 37 |
-
# O scaler retorna um array numpy (sem nome de colunas).
|
| 38 |
-
# Para evitar o "UserWarning: X does not have valid feature names", transformamos de volta em DataFrame:
|
| 39 |
-
scaled_df = pd.DataFrame(scaled_data, columns=expected_features)
|
| 40 |
-
|
| 41 |
-
cluster = int(kmeans.predict(scaled_df)[0])
|
| 42 |
-
proba = rf_model.predict_proba(scaled_df)[0]
|
| 43 |
-
|
| 44 |
-
return {
|
| 45 |
-
"cluster_id": cluster,
|
| 46 |
-
"persona": PERSONAS.get(cluster),
|
| 47 |
-
"confidence": f"{max(proba)*100:.2f}%"
|
| 48 |
-
}
|
|
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
+
from app.api.routes import router
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
app = FastAPI(title="Credit Card Clustering API")
|
| 5 |
|
| 6 |
+
# Inclui as rotas definidas em app/api/routes.py
|
| 7 |
+
app.include_router(router)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Inicializa a camada de serviços
|
app/services/prediction.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import joblib
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from app.core.config import settings
|
| 4 |
+
from app.personas import PERSONAS
|
| 5 |
+
|
| 6 |
+
class PredictionService:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
# Carrega os modelos uma única vez quando o serviço é instanciado
|
| 9 |
+
self.scaler = joblib.load(settings.SCALER_PATH)
|
| 10 |
+
self.kmeans = joblib.load(settings.KMEANS_PATH)
|
| 11 |
+
self.rf_model = joblib.load(settings.RF_PATH)
|
| 12 |
+
|
| 13 |
+
self.expected_features = [
|
| 14 |
+
'BALANCE_FREQUENCY', 'PURCHASES', 'CASH_ADVANCE',
|
| 15 |
+
'ONEOFF_PURCHASES_FREQUENCY', 'CREDIT_LIMIT', 'PRC_FULL_PAYMENT',
|
| 16 |
+
'TENURE', 'CREDIT_UTILIZATION', 'PAYMENT_RATIO'
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
def predict(self, data_dict: dict) -> dict:
|
| 20 |
+
df_input = pd.DataFrame([data_dict])
|
| 21 |
+
|
| 22 |
+
# Reordenar as colunas para o exato padrão que o Scikit-Learn espera no modelo treinado
|
| 23 |
+
df_input = df_input[self.expected_features]
|
| 24 |
+
|
| 25 |
+
# Normalizar
|
| 26 |
+
scaled_data = self.scaler.transform(df_input)
|
| 27 |
+
|
| 28 |
+
# Transformamos de volta em DataFrame para evitar warnings do Scikit-Learn
|
| 29 |
+
scaled_df = pd.DataFrame(scaled_data, columns=self.expected_features)
|
| 30 |
+
|
| 31 |
+
# Predição com K-Means e Confiança com Random Forest
|
| 32 |
+
cluster = int(self.kmeans.predict(scaled_df)[0])
|
| 33 |
+
proba = self.rf_model.predict_proba(scaled_df)[0]
|
| 34 |
+
|
| 35 |
+
return {
|
| 36 |
+
"cluster_id": cluster,
|
| 37 |
+
"persona": PERSONAS.get(cluster),
|
| 38 |
+
"confidence": f"{max(proba)*100:.2f}%"
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
# Instância Singleton para ser usada nos roteadores
|
| 42 |
+
prediction_service = PredictionService()
|
ui/components/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Inicializa a camada de componentes da UI
|
ui/components/forms.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
def render_input_form():
|
| 4 |
+
"""Renderiza o formulário de entrada e retorna o payload caso preenchido, ou None."""
|
| 5 |
+
st.subheader("📊 Insira os Dados Financeiros do Cliente")
|
| 6 |
+
|
| 7 |
+
col1, col2 = st.columns(2)
|
| 8 |
+
with col1:
|
| 9 |
+
bal_freq = st.slider("Frequência de Saldo (0 a 1)", 0.0, 1.0, 0.8, help="Proporção de meses em que o saldo da conta foi atualizado (1 = todos os meses).")
|
| 10 |
+
purchases = st.number_input("Total de Compras na Fatura ($)", 0.0, 50000.0, 1000.0, help="Valor total em dinheiro de todas as compras feitas pelo cliente.")
|
| 11 |
+
oneoff_freq = st.slider("Frequência de Compras à Vista (0 a 1)", 0.0, 1.0, 0.5, help="Frequência com que o cliente faz compras de parcela única (à vista).")
|
| 12 |
+
cash_adv = st.number_input("Saques em Espécie (Cash Advance - $)", 0.0, 50000.0, 0.0, help="Valor sacado em dinheiro vivo usando o limite do cartão.")
|
| 13 |
+
credit_limit = st.number_input("Limite de Crédito Total ($)", 0.0, 50000.0, 5000.0, help="O limite máximo de crédito disponível para o usuário.")
|
| 14 |
+
|
| 15 |
+
with col2:
|
| 16 |
+
prc_full = st.slider("Taxa de Pagamentos Integrais (0 a 1)", 0.0, 1.0, 0.0, help="Frequência em que o cliente paga o valor total da fatura (sem entrar no rotativo).")
|
| 17 |
+
tenure = st.selectbox("Tempo de Conta (Meses)", [6, 7, 8, 9, 10, 11, 12], index=6, help="Há quanto tempo (em meses) o cliente possui este cartão de crédito.")
|
| 18 |
+
cred_util = st.slider("Nível de Utilização de Crédito", 0.0, 2.0, 0.3, help="Relação entre o saldo devedor e o limite do cartão. Exemplo: 0.5 significa que usou 50% do limite. Se for 1.77, o cliente estourou o limite em 77%!")
|
| 19 |
+
pay_ratio = st.number_input("Proporção de Pagamento", 0.0, 100.0, 1.0, help="Proporção entre o valor que o cliente paga e a dívida real dele. Valores maiores indicam que o cliente abate mais a dívida.")
|
| 20 |
+
|
| 21 |
+
st.markdown("---")
|
| 22 |
+
|
| 23 |
+
# Se o botão for clicado, montamos o payload e retornamos
|
| 24 |
+
if st.button("🧠 Executar Motor de Inteligência", use_container_width=True):
|
| 25 |
+
return {
|
| 26 |
+
"BALANCE_FREQUENCY": bal_freq, "PURCHASES": purchases,
|
| 27 |
+
"ONEOFF_PURCHASES_FREQUENCY": oneoff_freq, "CASH_ADVANCE": cash_adv,
|
| 28 |
+
"CREDIT_LIMIT": credit_limit, "PRC_FULL_PAYMENT": prc_full,
|
| 29 |
+
"TENURE": tenure, "CREDIT_UTILIZATION": cred_util, "PAYMENT_RATIO": pay_ratio
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return None
|
ui/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Inicializa a camada de serviços da UI
|
ui/services/api_client.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
|
| 3 |
+
def predict_persona(payload: dict) -> dict:
|
| 4 |
+
"""Envia os dados do cliente para a API e retorna o resultado."""
|
| 5 |
+
try:
|
| 6 |
+
# Porta 8080 onde o FastAPI está rodando dentro do container
|
| 7 |
+
response = requests.post("http://127.0.0.1:8080/predict", json=payload)
|
| 8 |
+
|
| 9 |
+
if response.status_code == 200:
|
| 10 |
+
return {"success": True, "data": response.json()}
|
| 11 |
+
else:
|
| 12 |
+
return {"success": False, "error": "Erro ao processar dados na API."}
|
| 13 |
+
|
| 14 |
+
except Exception as e:
|
| 15 |
+
return {
|
| 16 |
+
"success": False,
|
| 17 |
+
"error": f"Erro de conexão com a API. Detalhes: {str(e)}"
|
| 18 |
+
}
|
ui/ui.py
CHANGED
|
@@ -1,48 +1,22 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
-
import
|
|
|
|
| 3 |
|
| 4 |
st.set_page_config(page_title="Inteligência de Personas", layout="wide")
|
| 5 |
|
| 6 |
st.title("💳 Inteligência de Personas de Clientes")
|
| 7 |
st.markdown("Descubra o perfil comportamental do seu cliente de cartão de crédito utilizando Machine Learning avançado.")
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
purchases = st.number_input("Total de Compras na Fatura ($)", 0.0, 50000.0, 1000.0, help="Valor total em dinheiro de todas as compras feitas pelo cliente.")
|
| 16 |
-
oneoff_freq = st.slider("Frequência de Compras à Vista (0 a 1)", 0.0, 1.0, 0.5, help="Frequência com que o cliente faz compras de parcela única (à vista).")
|
| 17 |
-
cash_adv = st.number_input("Saques em Espécie (Cash Advance - $)", 0.0, 50000.0, 0.0, help="Valor sacado em dinheiro vivo usando o limite do cartão.")
|
| 18 |
-
credit_limit = st.number_input("Limite de Crédito Total ($)", 0.0, 50000.0, 5000.0, help="O limite máximo de crédito disponível para o usuário.")
|
| 19 |
-
|
| 20 |
-
with col2:
|
| 21 |
-
prc_full = st.slider("Taxa de Pagamentos Integrais (0 a 1)", 0.0, 1.0, 0.0, help="Frequência em que o cliente paga o valor total da fatura (sem entrar no rotativo).")
|
| 22 |
-
tenure = st.selectbox("Tempo de Conta (Meses)", [6, 7, 8, 9, 10, 11, 12], index=6, help="Há quanto tempo (em meses) o cliente possui este cartão de crédito.")
|
| 23 |
-
cred_util = st.slider("Nível de Utilização de Crédito", 0.0, 2.0, 0.3, help="Relação entre o saldo devedor e o limite do cartão. Exemplo: 0.5 significa que usou 50% do limite. Se for 1.77, o cliente estourou o limite em 77%!")
|
| 24 |
-
pay_ratio = st.number_input("Proporção de Pagamento", 0.0, 100.0, 1.0, help="Proporção entre o valor que o cliente paga e a dívida real dele. Valores maiores indicam que o cliente abate mais a dívida.")
|
| 25 |
-
|
| 26 |
-
st.markdown("---")
|
| 27 |
-
|
| 28 |
-
if st.button("🧠 Executar Motor de Inteligência", use_container_width=True):
|
| 29 |
-
payload = {
|
| 30 |
-
"BALANCE_FREQUENCY": bal_freq, "PURCHASES": purchases,
|
| 31 |
-
"ONEOFF_PURCHASES_FREQUENCY": oneoff_freq, "CASH_ADVANCE": cash_adv,
|
| 32 |
-
"CREDIT_LIMIT": credit_limit, "PRC_FULL_PAYMENT": prc_full,
|
| 33 |
-
"TENURE": tenure, "CREDIT_UTILIZATION": cred_util, "PAYMENT_RATIO": pay_ratio
|
| 34 |
-
}
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
st.success(f"### 🎯 Resultado da Classificação: **{res['persona']}**")
|
| 43 |
-
st.metric("Confiança do Modelo (Random Forest)", res['confidence'])
|
| 44 |
-
else:
|
| 45 |
-
st.error("Erro ao processar dados na API.")
|
| 46 |
-
|
| 47 |
-
except Exception as e:
|
| 48 |
-
st.error("Erro de conexão com a API. Verifique se o backend (FastAPI) está rodando na porta 8080.")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
from components.forms import render_input_form
|
| 3 |
+
from services.api_client import predict_persona
|
| 4 |
|
| 5 |
st.set_page_config(page_title="Inteligência de Personas", layout="wide")
|
| 6 |
|
| 7 |
st.title("💳 Inteligência de Personas de Clientes")
|
| 8 |
st.markdown("Descubra o perfil comportamental do seu cliente de cartão de crédito utilizando Machine Learning avançado.")
|
| 9 |
|
| 10 |
+
# Renderiza o formulário e pega o payload caso o botão tenha sido clicado
|
| 11 |
+
payload = render_input_form()
|
| 12 |
|
| 13 |
+
if payload:
|
| 14 |
+
# Faz a requisição à API usando o cliente isolado
|
| 15 |
+
response = predict_persona(payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
if response["success"]:
|
| 18 |
+
res = response["data"]
|
| 19 |
+
st.success(f"### 🎯 Resultado da Classificação: **{res['persona']}**")
|
| 20 |
+
st.metric("Confiança do Modelo (Random Forest)", res['confidence'])
|
| 21 |
+
else:
|
| 22 |
+
st.error(response["error"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|