From 5afa7ffc85ce9cbae9009e0e56518e286609d2ad Mon Sep 17 00:00:00 2001 From: Nathan Souza Date: Fri, 20 Sep 2024 15:30:41 -0300 Subject: [PATCH 1/3] feat: adiciona codigo da API --- api/app/__init__.py | 0 api/app/main.py | 14 +++++++ api/app/routes.py | 88 ++++++++++++++++++++++++++++++++++++++++ api/app/schemas.py | 27 ++++++++++++ src/pipelines/predict.py | 4 +- 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 api/app/__init__.py create mode 100644 api/app/main.py create mode 100644 api/app/routes.py create mode 100644 api/app/schemas.py diff --git a/api/app/__init__.py b/api/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/app/main.py b/api/app/main.py new file mode 100644 index 0000000..2126a10 --- /dev/null +++ b/api/app/main.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI +from .routes import router +import os + +app = FastAPI() + +app.include_router(router) + +if __name__ == "__main__": + import uvicorn + + host = os.getenv("HOST", "127.0.0.1") + port = int(os.getenv("PORT", 8000)) + uvicorn.run(app, host=host, port=port) diff --git a/api/app/routes.py b/api/app/routes.py new file mode 100644 index 0000000..d59241d --- /dev/null +++ b/api/app/routes.py @@ -0,0 +1,88 @@ +import pandas as pd +from fastapi import APIRouter, HTTPException, File, UploadFile +from .schemas import PredictRequest, PredictionResponse +from config.settings import MODELS_DIR +from src.pipelines.predict import load_model, make_predictions, evaluate_predictions +from io import StringIO +import json + +router = APIRouter() +model_path = MODELS_DIR / "svc_model.joblib" + + +@router.post("/predict", response_model=PredictionResponse) +async def predict(input: PredictRequest): + data = input.to_dataframe() + + try: + model = load_model(model_path) + prediction = make_predictions(model, data) + result = int(prediction[0]) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Erro ao fazer a previsão: {str(e)}" + ) + + return PredictionResponse(prediction=result) + + +@router.post("/predict-batch", response_model=dict) +async def predict_batch(file: UploadFile = File(...)): + """Faz previsões em lote a partir de um arquivo CSV. + + O arquivo CSV deve conter as seguintes colunas: + - sepal_length: Comprimento da sépala. + - sepal_width: Largura da sépala. + - petal_length: Comprimento da pétala. + - petal_width: Largura da pétala. + """ + try: + contents = await file.read() + df = pd.read_csv(StringIO(contents.decode("utf-8"))) + model = load_model(model_path) + + predictions = make_predictions(model, df) + + return {"predictions": predictions.tolist()} + + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Erro ao processar o arquivo: {str(e)}" + ) + + +@router.post("/evaluate", response_model=dict) +async def predict_batch_with_evaluation(file: UploadFile = File(...)): + """Faz previsões em lote a partir de um arquivo CSV. + + O arquivo CSV deve conter as seguintes colunas: + - sepal_length: Comprimento da sépala. + - sepal_width: Largura da sépala. + - petal_length: Comprimento da pétala. + - petal_width: Largura da pétala. + - species: Rótulo verdadeiro da espécie (para avaliação). + """ + try: + contents = await file.read() + df = pd.read_csv(StringIO(contents.decode("utf-8"))) + model = load_model(model_path) + + if "species" not in df.columns: + raise HTTPException( + status_code=400, + detail="Os dados devem conter a coluna 'species' para avaliação.", + ) + + true_labels = df.pop("species") + predictions = make_predictions(model, df) + + report_dict = evaluate_predictions(predictions, true_labels) + report = json.dumps(report_dict, indent=4) + return { + "evaluation_report": json.loads(report), + } + + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Erro ao processar o arquivo: {str(e)}" + ) diff --git a/api/app/schemas.py b/api/app/schemas.py new file mode 100644 index 0000000..772ea7f --- /dev/null +++ b/api/app/schemas.py @@ -0,0 +1,27 @@ +import pandas as pd +from pydantic import BaseModel + + +class PredictRequest(BaseModel): + sepal_length: float + sepal_width: float + petal_length: float + petal_width: float + + def to_dataframe(self) -> pd.DataFrame: + """Prepara os dados como um DataFrame a partir do pedido.""" + return pd.DataFrame( + [ + [ + self.sepal_length, + self.sepal_width, + self.petal_length, + self.petal_width, + ] + ], + columns=["sepal_length", "sepal_width", "petal_length", "petal_width"], + ) + + +class PredictionResponse(BaseModel): + prediction: int diff --git a/src/pipelines/predict.py b/src/pipelines/predict.py index 9c9f39a..f657b0d 100644 --- a/src/pipelines/predict.py +++ b/src/pipelines/predict.py @@ -60,7 +60,9 @@ def evaluate_predictions(predictions: pd.Series, true_labels: pd.Series): Returns: str: Relatório de classificação. """ - return classification_report(true_labels, predictions) + return classification_report( + true_labels, predictions, output_dict=True, zero_division=0 + ) @app.command() From 51aac6afb5f3e7a3bc9332a0a1da55e25c328272 Mon Sep 17 00:00:00 2001 From: Nathan Souza Date: Mon, 23 Sep 2024 15:12:58 -0300 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20adiciona=20documenta=C3=A7=C3=A3o?= =?UTF-8?q?=20da=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 api/README.md diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..87c90d2 --- /dev/null +++ b/api/README.md @@ -0,0 +1,56 @@ +# Guia do Usuário API + +1. Certifique-se de ter o ambiente virtual ativado. +2. Instale as dependências usando o Poetry. +3. Navegue até a pasta principal do repositório. +4. Execute a API com o comando: + + ```bash + python -m api.app.main + ``` + +A API começará a rodar em [http://127.0.0.1:8000](http://127.0.0.1:8000) por padrão. Você pode usar ferramentas como curl, Postman ou um script Python para interagir com os endpoints. + +Além disso, você pode acessar a documentação da API em [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs), que é gerada automaticamente pelo FastAPI. + +## Endpoints da API + +### 1. `/predict` (POST) + +Prevê a espécie de uma flor de íris com base em uma única amostra de entrada. + +**Solicitação:** Informe os valores neste modelo: + +```json +{ + "sepal_length": 0, + "sepal_width": 0, + "petal_length": 0, + "petal_width": 0 +} +``` + +### 2. `/predict-batch` (POST) + +Processa um arquivo CSV contendo várias amostras para previsões em lote. + +**Solicitação:** Carregue um arquivo CSV com as seguintes colunas: + +- `sepal_length` +- `sepal_width` +- `petal_length` +- `petal_width` + +### 3. `/evaluate` (POST) + +Processa um arquivo CSV e avalia previsões em relação a rótulos verdadeiros. + +**Solicitação:** Carregue um arquivo CSV com as mesmas colunas acima, mais: + +- `species` (o verdadeiro rótulo para avaliação) + +## Estrutura do Código + +- **main.py**: O ponto de entrada do aplicativo, inicializa o aplicativo FastAPI e inclui o roteador para manipular solicitações. +- **routes.py**: Contém as definições para os endpoints da API, manipulando solicitações de previsões e avaliações. +- **schemas.py**: Define os modelos de dados para validação de entrada e saída usando Pydantic. \ No newline at end of file From 82009a02712a34c7b4a08ff746a553d46e7394b0 Mon Sep 17 00:00:00 2001 From: Nathan Souza Date: Thu, 26 Sep 2024 14:05:11 -0300 Subject: [PATCH 3/3] fix: ajusta nome do modelo --- api/app/routes.py | 2 +- artifacts/models/svc_model.joblib | Bin 5219 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 artifacts/models/svc_model.joblib diff --git a/api/app/routes.py b/api/app/routes.py index d59241d..e5bdaac 100644 --- a/api/app/routes.py +++ b/api/app/routes.py @@ -7,7 +7,7 @@ import json router = APIRouter() -model_path = MODELS_DIR / "svc_model.joblib" +model_path = MODELS_DIR / "model.joblib" @router.post("/predict", response_model=PredictionResponse) diff --git a/artifacts/models/svc_model.joblib b/artifacts/models/svc_model.joblib deleted file mode 100644 index 3faa58fa8283884f348ab6998e6b2446424ce51d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5219 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 T^m7pYPen&gii{R4n#lhEA2x__