Do dataset ao dashboard: construindo uma pipeline de Machine Learning ponta a ponta
No post anterior, apresentei o laboratório de previsão de demanda de bicicletas como um ecossistema: diferentes componentes tecnológicos que, quando articulados, transformam dados brutos em informação disponível para consumo por outros sistemas e por pessoas.
Neste post, o objetivo é percorrer tecnicamente essa arquitetura, mostrando como cada componente foi incorporado ao projeto.
A implementação seguiu aproximadamente este fluxo:
Bike Sharing Dataset
↓
Ingestão
↓
PostgreSQL
↓
Feature Engineering
↓
Dataset de treinamento
↓
Treinamento e avaliação
↓
Modelo persistido
↓
Prediction
├── FastAPI
└── previsões em lote
↓
PostgreSQL
↓
Vizro Dashboard
1. Estrutura do projeto e arquitetura
Antes da implementação da pipeline, foi criada uma estrutura modular para separar as responsabilidades do sistema.
O projeto foi inicializado com o uv, utilizado para gerenciamento do ambiente Python e das dependências:
uv initAs dependências foram incorporadas progressivamente à medida que os componentes passaram a ser desenvolvidos.
A arquitetura adotada separou responsabilidades em módulos como:
src/
├── api/
├── common/
├── dashboard/
├── features/
├── ingestion/
├── monitoring/
├── prediction/
└── training/
tests/
data/
models/
Os arquivos __init__.py foram adicionados aos pacotes Python.
A intenção não era simplesmente organizar arquivos visualmente. A divisão estabelecia limites entre as responsabilidades da aplicação: ingestão não deveria conhecer a interface do dashboard; o dashboard não deveria ser responsável por treinar o modelo; e a API deveria reutilizar o componente de predição em vez de implementar novamente a inferência.
Essa estrutura também permitiu que cada incremento fosse testado antes da implementação da etapa seguinte.
2. Ingestão de dados com SQLAlchemy e PostgreSQL
O estudo de caso utilizou o Bike Sharing Dataset, disponibilizado pelo UCI Machine Learning Repository.
A primeira etapa da pipeline consistiu em carregar os dados originais e verificar sua integridade antes de persistir qualquer informação.
Foram criados testes para validar características importantes do dataset, incluindo:
- carregamento do arquivo;
- quantidade esperada de colunas;
- unicidade do identificador
instant; - consistência entre
cnte seus componentes.
O teste isolado dessa camada foi executado com:
uv run pytest tests/test_raw_dataset.pyOs quatro testes foram aprovados.
Com a estrutura dos dados validada, o próximo componente foi o PostgreSQL.
A conexão foi centralizada no módulo common, utilizando SQLAlchemy e o driver Psycopg:
postgresql+psycopg://...
As informações sensíveis e específicas do ambiente foram retiradas do código e armazenadas em variáveis de ambiente no arquivo .env.
A partir dessas variáveis, a aplicação passou a construir a DATABASE_URL e criar o engine utilizado pelos demais módulos.
A ingestão foi então implementada no módulo correspondente, persistindo o dataset bruto no PostgreSQL.
Essa decisão criou uma separação importante:
Arquivo original
↓
Validação
↓
Raw data no PostgreSQL
O dado bruto passou a funcionar como a fonte persistida para as transformações posteriores, evitando que cada etapa da aplicação dependesse diretamente do arquivo original.
Uma dificuldade encontrada
Ao executar toda a suíte:
uv run pytestos testes relacionados ao dataset passaram, mas os testes de conexão e ingestão falharam.
A mensagem indicava:
connection refused
localhost:5432
O problema não estava no SQLAlchemy nem no código de ingestão: o PostgreSQL simplesmente não estava disponível naquele momento.
Esse problema acabou reforçando a necessidade de controlar também a infraestrutura da aplicação, algo que posteriormente seria resolvido com Docker Compose.
3. Feature Engineering
Com os dados brutos persistidos, a etapa seguinte foi construir uma representação apropriada para o modelo.
Foi criado um módulo específico de features.
A ideia foi manter duas camadas distintas no banco:
Raw Dataset
↓
Feature Engineering
↓
Training Dataset
Assim, os dados originais permanecem preservados enquanto outra representação é construída especificamente para Machine Learning.
Entre as informações utilizadas pelo modelo estavam características temporais e meteorológicas presentes no Bike Sharing Dataset, como hora, dia, mês, estação, feriado, temperatura e umidade.
Além das features, foi definido o atributo alvo correspondente à demanda de bicicletas.
Essa separação também evitou colocar transformações diretamente dentro do treinamento. O módulo de treinamento passou a receber um dataset já preparado para aquela finalidade.
4. Treinamento e avaliação do modelo
Com o dataset de treinamento disponível, iniciou-se a camada de Machine Learning.
Uma decisão importante foi respeitar a natureza temporal do problema.
Em vez de embaralhar aleatoriamente as observações, os dados foram separados temporalmente:
Período anterior
↓
Treinamento
Período posterior
↓
Teste
Isso permite avaliar uma situação mais próxima do uso real: utilizar o passado para estimar um período posterior.
Também foi utilizado um modelo baseline para estabelecer uma referência mínima de desempenho.
O modelo principal escolhido foi:
HistGradientBoostingRegressor
A avaliação utilizou três métricas:
MAE — Mean Absolute Error
Representa, em média, quantas bicicletas separam a previsão do valor observado.
RMSE — Root Mean Squared Error
Também representa erro em quantidade de bicicletas, mas penaliza erros maiores com mais intensidade.
R² — Coeficiente de determinação
Indica quanto da variabilidade observada é explicada pelo modelo.
No experimento realizado, o modelo chegou aproximadamente a:
MAE: 40,61 bicicletas
RMSE: 62,93 bicicletas
R²: 91,85%
O modelo treinado foi então persistido utilizando joblib.
Training Dataset
↓
HistGradientBoostingRegressor
↓
Avaliação
↓
.joblib
A partir desse momento, não era mais necessário treinar novamente o modelo para produzir cada previsão.
5. Prediction e API REST com FastAPI
O passo seguinte foi separar treinamento de inferência.
Foi criado um módulo de prediction responsável por carregar o modelo persistido e produzir previsões.
Essa separação permitiu que diferentes interfaces utilizassem o mesmo modelo.
Uma dessas interfaces foi uma API construída com FastAPI.
Conceitualmente:
Cliente
↓
POST /predict
↓
Pydantic
↓
Prediction
↓
Modelo .joblib
↓
Previsão
↓
JSON
O Pydantic foi utilizado para definir e validar a estrutura esperada na entrada da API.
O FastAPI também disponibilizou automaticamente a documentação interativa Swagger, permitindo inspecionar e testar os endpoints pelo navegador.
Assim, o modelo deixou de existir apenas como um objeto Python e passou a oferecer uma interface de comunicação para outros sistemas.
6. Testes automatizados com Pytest
Os testes não foram acrescentados apenas no final do projeto.
Eles começaram ainda na configuração do PostgreSQL e acompanharam a evolução da pipeline.
O Pytest foi adicionado como dependência de desenvolvimento e configurado para localizar o projeto e os testes corretamente.
A configuração utilizada incluiu:
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]O laboratório passou a possuir testes para diferentes responsabilidades, entre elas:
configuração
dataset bruto
conexão com banco
ingestão
pipeline
API
A execução completa era realizada com:
uv run pytestEsse ponto foi especialmente importante porque os testes não verificaram apenas algoritmos.
Eles também permitiram identificar problemas de infraestrutura. Foi justamente um teste que evidenciou que o PostgreSQL não estava disponível durante uma das primeiras execuções.
Dessa forma, os testes passaram a funcionar como mecanismo de confiança sobre diferentes partes do ecossistema.
7. Docker Compose e ambiente reproduzível
À medida que a solução cresceu, passou a existir um problema diferente.
Já não bastava executar um script Python.
O laboratório precisava coordenar:
PostgreSQL
FastAPI
Vizro
O PostgreSQL inicialmente precisava estar disponível separadamente para que os testes e a ingestão funcionassem.
Para controlar essa infraestrutura foi adotado Docker Compose.
O compose.yaml passou a definir os serviços do laboratório.
O PostgreSQL foi configurado utilizando variáveis de ambiente:
environment:
POSTGRES_DB: ${DATABASE_NAME}
POSTGRES_USER: ${DATABASE_USER}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}Foi utilizado um volume persistente:
volumes:
- bike_postgres_data:/var/lib/postgresql/dataTambém foi criado um healthcheck com pg_isready.
Isso foi particularmente importante porque a API não deveria simplesmente iniciar alguns segundos depois do banco. Ela deveria iniciar quando o PostgreSQL estivesse efetivamente pronto para receber conexões.
A dependência passou a ser expressa como:
depends_on:
postgres:
condition: service_healthyPosteriormente, API e dashboard foram incorporados ao mesmo ambiente.
Assim, o laboratório passou a ser iniciado por:
docker compose up -de encerrado com:
docker compose downO Docker Compose transformou aquilo que antes era uma sequência manual de inicializações em uma descrição reproduzível do ecossistema.
8. Previsões, persistência e dashboard com Vizro
A última etapa foi tornar o comportamento do modelo observável.
Para isso, não bastava manter a inferência disponível somente pela API.
Foi criado um processo para gerar previsões sobre o conjunto temporal reservado para teste.
O módulo carregava os dados, realizava o mesmo temporal_train_test_split, carregava o modelo persistido e produzia uma estrutura contendo:
timestamp
actual
predicted
absolute_error
Essas informações foram persistidas em uma tabela PostgreSQL:
bike_demand_predictions
A partir dela, o Vizro passou a consultar os resultados e apresentá-los visualmente.
O dashboard incorporou indicadores de desempenho, entre eles:
Erro Médio (MAE)
40,61 bicicletas
Erro Quadrático (RMSE)
62,93 bicicletas
Precisão do Modelo (R²)
91,85%
Além dos indicadores, a visualização permitiu observar a relação entre demanda real e prevista ao longo do período.
O dashboard, portanto, não executa o treinamento.
Ele ocupa outra posição na arquitetura:
Modelo
↓
Predictions
↓
PostgreSQL
↓
Vizro
↓
Interpretação
Essa distinção é importante porque separa produção de informação de sua visualização.
O ecossistema completo
Ao final do laboratório, aquilo que inicialmente era apenas um dataset passou a constituir um pequeno sistema de Machine Learning:
Bike Sharing Dataset
↓
Ingestion
↓
PostgreSQL
↓
Feature Engineering
↓
Training Dataset
↓
Model Training
↓
joblib
↙ ↘
FastAPI Batch Prediction
↓
PostgreSQL
↓
Vizro
Cada componente possui uma responsabilidade específica, mas nenhum deles explica sozinho o funcionamento do sistema.
O modelo não possui utilidade operacional sem dados adequadamente preparados. A API não produz previsões sem o modelo. O dashboard não apresenta resultados sem as previsões persistidas. E todos esses componentes dependem de uma infraestrutura capaz de mantê-los disponíveis e integrados.
É nesse sentido que retorno à ideia apresentada no post anterior: o objeto computacional não está apenas nos componentes, mas também nas relações estabelecidas entre eles.
O resultado técnico deste laboratório não foi somente um modelo capaz de prever demanda de bicicletas. Foi a construção de um fluxo reproduzível que transforma dados brutos em informação disponível para interpretação.
Visite o repositório do labs, baixe e execute o projeto para reproduzir o fluxo completo de Machine Learning.
Reproduzindo o laboratório
O código-fonte completo deste laboratório está disponível no repositório do projeto no GitHub. Disponível em: https://github.com/danicarvalhomf/python-especialista/tree/main/labs/lab-bike-demand-mlops
Para reproduzir o ambiente e executar o fluxo completo de Machine Learning em sua máquina, consulte o README.md do laboratório, que contém os requisitos, a configuração das variáveis de ambiente e os comandos necessários para inicialização dos serviços e execução da pipeline.
O projeto foi estruturado para permitir a reprodução do ecossistema por meio do Docker Compose, incluindo PostgreSQL, API FastAPI e dashboard Vizro.