YanEnrique commited on
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 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 joblib
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
- @app.get("/")
19
- def home():
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 requests
 
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
- # Interface de entrada
10
- st.subheader("📊 Insira os Dados Financeiros do Cliente")
11
 
12
- col1, col2 = st.columns(2)
13
- with col1:
14
- 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).")
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
- try:
37
- # Porta 8080 onde o FastAPI está rodando dentro do container
38
- response = requests.post("http://127.0.0.1:8080/predict", json=payload)
39
-
40
- if response.status_code == 200:
41
- res = response.json()
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"])