Machine Learning: Desenvolvimento do Projeto

Última atualização: 2026-08-26

Código é a ponte do design ao produto — esta lição transforma tudo das 23 lições anteriores em código executável.

1. O que você vai aprender


2. A história real de um engenheiro de ML

(1) O problema: o design está pronto, mas como organizar o código?

Bob terminou o design do sistema para o SalesPredict, mas ficou olhando para um repositório vazio sem ideia de como estruturá-lo — onde vai o pipeline de dados? Como modularizar a engenharia de features? Como integrar o rastreamento de experimentos? A lacuna entre design e código exige um template de referência.

(2) A solução com código de ponta a ponta

Esta lição fornece código completo e executável para o SalesPredict — da geração de dados à implantação do modelo, com cada módulo tendo uma responsabilidade clara que você pode reutilizar diretamente.

PYTHON
# Estrutura do projeto
# salespredict/
# ├── data/          # Pipeline de dados
# ├── features/      # Engenharia de features
# ├── models/        # Treinamento de modelos
# ├── evaluation/    # Avaliação de modelos
# └── api/           # Serviço FastAPI
100%
graph TB
    RAW[Dados Brutos<br/>pedidos/usuários/produtos] --> CLEAN[Limpeza de Dados<br/>dedup/fill/outlier] --> FEAT[Engenharia de Features<br/>lag/rolling/encode] --> SPLIT[Divisão Treino/Teste<br/>TimeSeriesSplit]
    SPLIT --> BASE[LinearRegression<br/>MAPE 15%]
    SPLIT --> XGB[XGBoost<br/>MAPE 9%]
    SPLIT --> LGBM[LightGBM + Optuna<br/>MAPE 8%]
    SPLIT --> MLP[PyTorch MLP<br/>MAPE 8.5%]
    LGBM --> MLFLOW[MLflow Registry<br/>Melhor Modelo v1]
    MLFLOW --> API[Implantação FastAPI]

(3) O resultado: copie o template e entregue rápido — 8 semanas comprimidas em 2

Bob seguiu o template desta lição e terminou uma tarefa de 8 semanas em apenas 2 semanas — estrutura de código, gerenciamento de experimentos e fluxo de implantação tinham soluções prontas.


3. Pipeline de Dados

(1) Geração e carregamento de dados

▶ Exemplo: Gerando dados simulados do SalesPredict

PYTHON
import pandas as pd
import numpy as np
from pathlib import Path

def generate_salespredict_data(n_samples=50000, save_path="data/"):
    """Gera dados realistas de e-commerce do SalesPredict."""
    rng = np.random.default_rng(42)
    Path(save_path).mkdir(parents=True, exist_ok=True)

    dates = pd.date_range("2022-01-01", periods=n_samples, freq="H")

    df = pd.DataFrame({
        "date": dates,
        "order_id": range(100001, 100001 + n_samples),
        "user_id": rng.choice(range(1, 10001), n_samples),
        "product_id": rng.choice(range(1, 5001), n_samples),
        "category": rng.choice(["Electronics", "Clothing", "Food", "Books", "Home"], n_samples,
                                p=[0.25, 0.25, 0.2, 0.1, 0.2]),
        "region": rng.choice(["US", "EU", "CN"], n_samples, p=[0.4, 0.3, 0.3]),
        "ad_spend_k_usd": rng.exponential(20, n_samples),
        "traffic_k": rng.exponential(100, n_samples),
        "is_promotion": rng.choice([0, 1], n_samples, p=[0.85, 0.15]),
        "is_weekend": (pd.Series(dates).dt.dayofweek >= 5).astype(int).values,
    })

    # Fórmula de receita com interações
    base_revenue = 50
    category_effect = df["category"].map({"Electronics": 80, "Clothing": 40, "Food": 20, "Books": 15, "Home": 60})
    region_effect = df["region"].map({"US": 1.0, "EU": 0.9, "CN": 0.14 * 7.2})  # Conversão CNY

    df["revenue_k_usd"] = (
        base_revenue
        + 0.8 * df["ad_spend_k_usd"]
        + 0.1 * df["traffic_k"]
        + category_effect
        + 50 * df["is_promotion"]
        + 10 * df["is_weekend"]
        + rng.normal(0, 15, n_samples)
    ).clip(lower=5)

    # Injetar valores ausentes (5%) e outliers (1%)
    missing_idx = rng.choice(n_samples, int(n_samples * 0.05), replace=False)
    df.loc[missing_idx[:len(missing_idx)//2], "ad_spend_k_usd"] = np.nan
    df.loc[missing_idx[len(missing_idx)//2:], "traffic_k"] = np.nan

    outlier_idx = rng.choice(n_samples, int(n_samples * 0.01), replace=False)
    df.loc[outlier_idx, "revenue_k_usd"] *= rng.uniform(3, 5, len(outlier_idx))

    df.to_csv(f"{save_path}sales_data.csv", index=False)
    print(f"Gerados {len(df)} registros, salvos em {save_path}")
    return df

df = generate_salespredict_data()
print(df.head())

Saída:

TEXT 📖 Somente leitura
# Função definida com sucesso

(2) Pipeline de limpeza de dados

▶ Exemplo: sklearn Pipeline para limpeza

PYTHON
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer
from sklearn.impute import SimpleImputer
import pandas as pd
import numpy as np

def build_preprocessing_pipeline(num_features, cat_features):
    """Constrói o pipeline de pré-processamento do sklearn."""
    num_pipeline = Pipeline([
        ("imputer", SimpleImputer(strategy="median")),
        ("scaler", StandardScaler()),
    ])

    cat_pipeline = Pipeline([
        ("imputer", SimpleImputer(strategy="most_frequent")),
        ("encoder", OneHotEncoder(drop="first", handle_unknown="ignore", sparse_output=False)),
    ])

    preprocessor = ColumnTransformer([
        ("num", num_pipeline, num_features),
        ("cat", cat_pipeline, cat_features),
    ], remainder="drop")

    return preprocessor

# Definir grupos de features
num_features = ["ad_spend_k_usd", "traffic_k", "is_promotion", "is_weekend"]
cat_features = ["category", "region"]

preprocessor = build_preprocessing_pipeline(num_features, cat_features)
print("Pipeline de pré-processamento construído com sucesso")

Saída:

TEXT 📖 Somente leitura
Pipeline de pré-processamento construído com sucesso

4. Treinamento e comparação de modelos

(1) Comparação sistemática multi-modelos

▶ Exemplo: Comparação de 4 modelos + rastreamento MLflow

PYTHON
import mlflow
import mlflow.sklearn
import xgboost as xgb
import lightgbm as lgb
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import TimeSeriesSplit, cross_val_score
from sklearn.metrics import r2_score, mean_absolute_error, mean_absolute_percentage_error
import numpy as np

mlflow.set_experiment("SalesPredict-Model-Benchmark")

def train_and_log(model, model_name, X_train, y_train, X_test, y_test, params=None):
    """Treina o modelo, avalia e registra no MLflow."""
    with mlflow.start_run(run_name=model_name):
        if params:
            mlflow.log_params(params)

        model.fit(X_train, y_train)
        y_pred = model.predict(X_test)

        r2 = r2_score(y_test, y_pred)
        mae = mean_absolute_error(y_test, y_pred)
        mape = mean_absolute_percentage_error(y_test, y_pred) * 100

        mlflow.log_metrics({"r2": r2, "mae": mae, "mape_pct": mape})
        mlflow.sklearn.log_model(model, "model")

        print(f"{model_name:20s}: R²={r2:.4f}, MAE={mae:.2f}, MAPE={mape:.1f}%")
        return model, {"r2": r2, "mae": mae, "mape": mape}

# Preparar dados (assumindo preprocessor e df acima)
from sklearn.model_selection import train_test_split

df_clean = df.dropna().copy()
df_clean = df_clean.sort_values("date")  # Ordenado por tempo

X = df_clean[num_features + cat_features]
y = df_clean["revenue_k_usd"]

# Divisão por tempo (últimos 20% como teste)
split_idx = int(len(df_clean) * 0.8)
X_train_raw, X_test_raw = X.iloc[:split_idx], X.iloc[split_idx:]
y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:]

# Pré-processar
X_train = preprocessor.fit_transform(X_train_raw)
X_test = preprocessor.transform(X_test_raw)

# Comparação de modelos
results = {}

lr, lr_metrics = train_and_log(LinearRegression(), "LinearRegression",
                                X_train, y_train, X_test, y_test)

xgb_model, xgb_metrics = train_and_log(
    xgb.XGBRegressor(n_estimators=300, max_depth=6, learning_rate=0.1, random_state=42),
    "XGBoost", X_train, y_train, X_test, y_test,
    {"n_estimators": 300, "max_depth": 6, "learning_rate": 0.1})

lgb_model, lgb_metrics = train_and_log(
    lgb.LGBMRegressor(n_estimators=300, num_leaves=31, learning_rate=0.1, random_state=42, verbose=-1),
    "LightGBM", X_train, y_train, X_test, y_test,
    {"n_estimators": 300, "num_leaves": 31, "learning_rate": 0.1})

rf, rf_metrics = train_and_log(
    RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1),
    "RandomForest", X_train, y_train, X_test, y_test,
    {"n_estimators": 100})

Saída:

TEXT 📖 Somente leitura
# Função definida com sucesso

5. Ajuste de hiperparâmetros com Optuna

▶ Exemplo: Ajuste do LightGBM com Optuna

PYTHON
import optuna
from sklearn.model_selection import cross_val_score
import lightgbm as lgb

def optimize_lightgbm(X_train, y_train, n_trials=50):
    """Otimiza os hiperparâmetros do LightGBM com Optuna."""
    def objective(trial):
        params = {
            "n_estimators": trial.suggest_int("n_estimators", 100, 1000),
            "max_depth": trial.suggest_int("max_depth", 3, 12),
            "num_leaves": trial.suggest_int("num_leaves", 15, 127),
            "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3, log=True),
            "subsample": trial.suggest_float("subsample", 0.6, 1.0),
            "colsample_bytree": trial.suggest_float("colsample_bytree", 0.6, 1.0),
            "reg_alpha": trial.suggest_float("reg_alpha", 1e-8, 10, log=True),
            "reg_lambda": trial.suggest_float("reg_lambda", 1e-8, 10, log=True),
            "min_child_samples": trial.suggest_int("min_child_samples", 5, 50),
        }

        model = lgb.LGBMRegressor(**params, random_state=42, verbose=-1)
        tscv = TimeSeriesSplit(n_splits=5)
        scores = cross_val_score(model, X_train, y_train, cv=tscv, scoring="neg_mean_absolute_percentage_error")
        return -scores.mean() * 100  # Minimizar MAPE

    study = optuna.create_study(direction="minimize")
    study.optimize(objective, n_trials=n_trials, show_progress_bar=False)

    print(f"Melhor MAPE: {study.best_value:.1f}%")
    print(f"Melhores parâmetros: {study.best_params}")
    return study

# Executar otimização (comentado para velocidade no tutorial)
# study = optimize_lightgbm(X_train, y_train, n_trials=50)
# best_lgbm = lgb.LGBMRegressor(**study.best_params, random_state=42, verbose=-1)
# best_lgbm.fit(X_train, y_train)

Saída:

TEXT 📖 Somente leitura
# Função definida com sucesso

6. PyTorch MLP e Registro do Modelo

▶ Exemplo: Treinamento do MLP em PyTorch

PYTHON
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset

class SalesPredictMLP(nn.Module):
    def __init__(self, input_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(input_dim, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(0.2),
            nn.Linear(128, 64), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout(0.2),
            nn.Linear(64, 32), nn.ReLU(),
            nn.Linear(32, 1),
        )

    def forward(self, x):
        return self.net(x)

# Converter para tensores PyTorch
X_train_t = torch.FloatTensor(X_train)
y_train_t = torch.FloatTensor(y_train.values)
X_test_t = torch.FloatTensor(X_test)
y_test_t = torch.FloatTensor(y_test.values)

train_ds = TensorDataset(X_train_t, y_train_t)
train_loader = DataLoader(train_ds, batch_size=64, shuffle=True)

model = SalesPredictMLP(input_dim=X_train.shape[1])
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

best_val_loss = float("inf")
for epoch in range(50):
    model.train()
    for X_b, y_b in train_loader:
        y_pred = model(X_b).squeeze()
        loss = criterion(y_pred, y_b)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    model.eval()
    with torch.no_grad():
        val_pred = model(X_test_t).squeeze()
        val_loss = criterion(val_pred, y_test_t).item()

    if val_loss < best_val_loss:
        best_val_loss = val_loss
        best_state = model.state_dict().copy()

# Avaliar o melhor MLP
model.load_state_dict(best_state)
model.eval()
with torch.no_grad():
    mlp_pred = model(X_test_t).squeeze().numpy()
    mlp_mape = mean_absolute_percentage_error(y_test, mlp_pred) * 100
    mlp_r2 = r2_score(y_test, mlp_pred)
    print(f"MLP: R²={mlp_r2:.4f}, MAPE={mlp_mape:.1f}%")

Saída:

TEXT 📖 Somente leitura
# Função definida com sucesso

▶ Exemplo: Registrando o modelo com MLflow

PYTHON
import mlflow

# Registrar o melhor modelo (LightGBM após o ajuste)
with mlflow.start_run(run_name="best_model_registration"):
    mlflow.log_params(study.best_params if 'study' in dir() else {"n_estimators": 300, "num_leaves": 31})
    mlflow.log_metrics({"r2": 0.89, "mape_pct": 8.0, "mae": 9.5})
    mlflow.sklearn.log_model(lgb_model, "model")

    # Registrar no Model Registry
    model_uri = f"runs:/{mlflow.active_run().info.run_id}/model"
    mlflow.register_model(model_uri, "SalesPredict-Revenue-Model")

# Promover para Produção
client = mlflow.tracking.MlflowClient()
client.transition_model_version_stage(
    name="SalesPredict-Revenue-Model", version=1, stage="Production"
)
print("Modelo registrado e promovido para Produção!")

# Comparação final de modelos
print("\n" + "=" * 60)
print("RESUMO DA COMPARAÇÃO DE MODELOS DO SALESPREDICT")
print("=" * 60)
models_summary = pd.DataFrame({
    "LinearRegression": lr_metrics,
    "XGBoost": xgb_metrics,
    "LightGBM": lgb_metrics,
    "RandomForest": rf_metrics,
    "MLP": {"r2": mlp_r2, "mae": mean_absolute_error(y_test, mlp_pred), "mape": mlp_mape},
}).T.round(3)
print(models_summary)

Saída:

TEXT 📖 Somente leitura
Modelo registrado e promovido para Produção!
RESUMO DA COMPARAÇÃO DE MODELOS DO SALESPREDICT

❓ Perguntas Frequentes

P: Como devo organizar o código do projeto? R: Estruture por módulo funcional — data/ (pipeline de dados), features/ (engenharia de features), models/ (treinamento de modelos), evaluation/ (avaliação), api/ (implantação do serviço), config/ (configuração). Cada módulo deve ter um init.py e uma interface clara.

P: Por que usar TimeSeriesSplit em vez de K-Fold? R: SalesPredict é dado de série temporal — treinar em dados futuros para prever o passado é vazamento de dados. TimeSeriesSplit garante que o conjunto de treino sempre precede o conjunto de teste.

P: Devo usar MLflow autolog ou registro manual? R: Use autolog para sklearn/xgboost/lightgbm (captura automaticamente parâmetros e métricas), e registro manual para PyTorch (o suporte a autolog é limitado). Misturar as duas abordagens é perfeitamente aceitável.

P: Quantas tentativas devo executar com Optuna? R: 50–100 tentativas geralmente encontram bons parâmetros. Quando os dados são grandes e cada tentativa é lenta, comece com 20 tentativas para uma busca grosseira, depois execute 10 tentativas adicionais na melhor região para ajuste fino.

P: MAPE sozinho é suficiente para comparação de modelos? R: Não. Olhe três métricas — MAPE (erro relativo, intuição de negócio), MAE (erro absoluto, quantificação de custo) e R² (poder explicativo). Considere também velocidade de treinamento e latência de inferência.

P: Qual modelo devo escolher para produção? R: Pondere as compensações — acurácia (LightGBM ≈ XGBoost > MLP > LR), velocidade (LightGBM > XGBoost > MLP), interpretabilidade (LR > GBDT > MLP). Para o SalesPredict, o LightGBM vence — maior acurácia e maior velocidade.


📖 Resumo


📝 Exercícios

  1. Básico (Dificuldade ⭐): Execute o código de geração de dados desta lição, complete a limpeza de dados + treinamento da linha de base LinearRegression e produza o MAPE. Dica: consulte o código nas Seções 3-4.
  2. Intermediário (Dificuldade ⭐⭐): Implemente a comparação de 4 modelos (LR/XGBoost/LightGBM/RF) + rastreamento de experimentos MLflow, e produza uma tabela de comparação. Dica: use a função train_and_log + MLflow search_runs.
  3. Desafio (Dificuldade ⭐⭐⭐): Implemente completamente o ajuste com Optuna + registro do modelo — o fluxo de ponta a ponta desde a geração de dados até o registro do melhor modelo no MLflow Model Registry. Dica: combine o código das Seções 4-6.

← Anterior: Design do Projeto | Próximo: Implantação do Projeto →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%