Machine Learning · Engenharia de Dados
Previsão de churn em escala: um pipeline de ML distribuído
Da pergunta de negócio ao score de risco por cliente: aprendizado supervisionado, comparação de três algoritmos de classificação e arquitetura de dados em camadas sobre Spark, Databricks e MLflow.
Operadoras de telecom perdem clientes continuamente para o cancelamento — o churn — e adquirir um cliente novo custa várias vezes mais do que reter um existente. A pergunta de negócio do projeto é:
“Quais clientes têm maior probabilidade de cancelar nos próximos meses — e por quê?”
Responder exige mais do que treinar um modelo: exige um pipeline de ponta a ponta que lide com dado sujo, escala distribuída, rastreabilidade de experimentos e promoção governada. Este artigo percorre cada etapa — sobre Apache Spark, Databricks, Delta Lake, MLflow e Unity Catalog — com foco nos algoritmos e nas decisões que sustentam o resultado.
Supervisionado ou não supervisionado?
A primeira decisão de modelagem é o tipo de aprendizado, porque ela determina os algoritmos disponíveis, as métricas válidas e o formato do dado.
No aprendizado não supervisionado, os dados não têm rótulos: o algoritmo
descobre estrutura sozinho — agrupa clientes parecidos (clustering, como o
k-means), reduz dimensionalidade (PCA) ou detecta anomalias.
Não há rótulo a prever, apenas padrões a identificar.
No aprendizado supervisionado, cada exemplo vem com um rótulo, e o
algoritmo aprende a função que mapeia as variáveis de entrada para esse rótulo. É o caso aqui:
o dataset tem a coluna Churn indicando, para cada cliente histórico,
se ele cancelou. Como o rótulo assume só dois valores — cancelou (1)
ou permaneceu (0) — trata-se de classificação binária.
O rótulo discreto distingue o problema de uma regressão (valor contínuo) e habilita
métricas como AUC-ROC e F1.
A segmentação de clientes na análise exploratória (por contrato, por tempo de casa) é
descritiva, guiada por regras de negócio — não são clusters
aprendidos por um algoritmo. Uma evolução do projeto seria rodar um
k-means para descobrir segmentos latentes de risco, como
complemento ao classificador.
Arquitetura Medallion
A qualidade do modelo é limitada pela qualidade do dado, e dado de produção chega sujo. A arquitetura Medallion organiza o fluxo em três camadas sobre o Delta Lake, cada uma com uma responsabilidade definida — permitindo reprocessar, auditar e reutilizar o dado sem reescrever o pipeline inteiro.
Dado bruto, intocado
O CSV como chegou, sem filtro nem transformação semântica. Garante rastreabilidade e permite reprocessar do zero.
Limpo e confiável
Nulos tratados, tipos corrigidos, target binarizado, regras de qualidade aplicadas. Pronto para análise e reutilizável por qualquer caso de uso.
Features para ML
Variáveis derivadas e codificadas no formato que o modelo consome. É a única camada acoplada a este caso de uso.
Delta Lake em vez de Parquet puro pelas transações ACID, versionamento e time travel — consultar o estado da tabela em qualquer versão anterior. Em um pipeline reexecutado dezenas de vezes, isso garante consistência e reprodutibilidade.
ETAPA 01 — INGESTÃO
Bronze: ingestão sem transformação
A camada Bronze lê a tabela-fonte registrada no Unity Catalog e a persiste em Delta sem transformação, apenas com casting mínimo de tipos. Nada que altere o significado do dado entra aqui. Com o Bronze imutável, um bug na limpeza se corrige reprocessando, sem perda de dado.
# Lê a fonte pelo nome no Unity Catalog e persiste em Delta, sem tocar no conteúdo df_raw = spark.table("portfolio.default.wa_fn_use_c_telco_customer_churn") (df_raw.write .format("delta") .mode("overwrite") .option("overwriteSchema", "true") .saveAsTable("portfolio.default.telco_bronze")) # 7.043 linhas preservadas
ETAPA 02 — LIMPEZA
Prata: limpeza e padronização
A camada Prata define o teto de qualidade do modelo. O dataset Telco tem um problema conhecido:
a coluna TotalCharges chega como texto, e clientes com
tenure = 0 (recém-chegados, sem cobrança acumulada) trazem um espaço
em branco " " no lugar do número. Um cast
direto produziria nulo silencioso. O tratamento é explícito: espaço vazio vira
0.0, o valor semanticamente correto.
Três decisões definem esta camada:
1. Binarizar o alvo. Churn “Yes”/“No” vira
1/0, formato numérico exigido pela
labelCol dos classificadores do Spark ML.
2. Remover o customerID. Identificadores únicos não
têm poder preditivo e abrem porta para data leakage: o modelo decoraria clientes em
vez de aprender padrões generalizáveis.
3. Normalizar categorias. Um trim() nas strings
evita que "Yes" e "Yes " virem
categorias distintas, o que corromperia o encoding adiante.
A binarização revela que cerca de 26% dos clientes deram churn — um desbalanceamento de classes, com a classe-alvo na minoria. Esse número determina a escolha da métrica de avaliação mais adiante.
# Espaço em branco em TotalCharges → 0.0 (clientes com tenure = 0 sem cobrança) df_silver = (df_bronze .withColumn("TotalCharges", F.when(F.trim(F.col("TotalCharges")) == "", F.lit(0.0)) .otherwise(F.col("TotalCharges").cast("double"))) .withColumn("Churn", (F.col("Churn") == F.lit("Yes")).cast("int")) # "Yes"/"No" → 1/0 .drop("customerID")) # sem poder preditivo (df_silver.write .format("delta").mode("overwrite") .option("overwriteSchema", "true") .saveAsTable("portfolio.default.telco_silver"))
ETAPA 03 — EXPLORAÇÃO
Análise exploratória (EDA com Spark SQL)
A análise exploratória foi escrita em Spark SQL sobre a camada Prata — linguagem declarativa rodando sobre um motor que escala para terabytes. Cada consulta testou uma hipótese de negócio, e os padrões resultantes definiram as features posteriores.
Cada insight vira uma feature em potencial — tempo e tipo de contrato, serviço de internet, método de pagamento, número de serviços e cobrança mensal — base da engenharia de features adiante.
-- Taxa de churn por tipo de contrato — hipótese de fidelização SELECT Contract, COUNT(*) AS total, SUM(Churn) AS churned, ROUND(AVG(Churn) * 100, 1) AS churn_pct FROM portfolio.default.telco_silver GROUP BY Contract ORDER BY churn_pct DESC -- Resultado: Month-to-month 42,7% · One year 11,3% · Two year 2,8%
ETAPA 04 — ENGENHARIA DE FEATURES
Ouro: engenharia de features
Algoritmos operam sobre números, não sobre categorias como “contrato mês a mês” ou “fibra
ótica”. A camada Ouro transforma o dado limpo em um vetor numérico denso via um
Spark ML Pipeline de três estágios. O Pipeline garante reprodutibilidade:
fit() aprende os parâmetros (o vocabulário de categorias, por
exemplo) e transform() os aplica; o mesmo objeto serializado é
recarregado na inferência.
Os três estágios de codificação
StringIndexer — converte cada categoria em um índice inteiro, ordenado por
frequência (o valor mais comum recebe 0).
handleInvalid="keep" reserva um índice para categorias não vistas
no treino, evitando que o pipeline quebre em produção diante de um valor novo.
OneHotEncoder — o índice inteiro introduz uma ordem falsa
(2 > 1 > 0, inexistente entre “cartão”, “boleto” e “cheque”).
O one-hot remove essa ordem, transformando cada índice em um vetor binário esparso.
dropLast=True descarta uma categoria por variável para evitar a
dummy variable trap — a multicolinearidade que desestabiliza modelos lineares.
VectorAssembler — reúne as categóricas codificadas e as numéricas em uma única
coluna features, o vetor consumido pelos algoritmos.
Três features de domínio
Além das 21 variáveis originais, três features derivadas da EDA traduzem conhecimento de negócio em colunas:
| Feature | O que captura |
|---|---|
num_services | Contagem de serviços adicionais (0–8). Correlaciona negativamente com churn — mais produtos, mais retenção. |
charges_per_tenure | TotalCharges / tenure: custo médio mensal acumulado, proxy do valor percebido ao longo do tempo. |
is_new_customer | Flag para os primeiros 12 meses — o período com ~47,7% de churn identificado na EDA. |
Essas três features são criadas fora do pipeline Spark ML — ele as consome, mas não as gera. Por isso precisam ser recriadas antes de qualquer inferência em dados novos. Omitir essa etapa causa training-serving skew: o modelo recebe um vetor com formato diferente do treino e produz predições inválidas sem erro aparente.
# Features de domínio — derivadas dos padrões encontrados na EDA service_cols = ["PhoneService", "MultipleLines", "OnlineSecurity", "OnlineBackup", "DeviceProtection", "TechSupport", "StreamingTV", "StreamingMovies"] df_gold = (df_silver .withColumn("num_services", sum(F.when(F.col(c) == "Yes", 1).otherwise(0) for c in service_cols)) .withColumn("charges_per_tenure", F.col("TotalCharges") / F.greatest(F.col("tenure"), F.lit(1))) .withColumn("is_new_customer", (F.col("tenure") <= 12).cast("int"))) # Pipeline: StringIndexer → OneHotEncoder → VectorAssembler indexers = [StringIndexer(inputCol=c, outputCol=c+"_idx", handleInvalid="keep") for c in cat_cols] encoders = [OneHotEncoder(inputCol=c+"_idx", outputCol=c+"_ohe", dropLast=True) for c in cat_cols] assembler = VectorAssembler(inputCols=num_cols + [c+"_ohe" for c in cat_cols], outputCol="features") df_features = Pipeline(stages=indexers + encoders + [assembler]).fit(df_gold).transform(df_gold)
ETAPA 05 — TREINAMENTO
Os três classificadores
Com o dataset Ouro pronto, o split é 80% treino / 20% teste
(seed=42), preservando a proporção de churn em cada parte. São
treinados três classificadores supervisionados, cada um com uma abordagem
distinta para separar “permanece” de “cancela”, todos rodando distribuídos via
Spark MLlib — não em scikit-learn de nó único.
1 · Regressão Logística
linear · interpretávelO baseline. Modela a probabilidade de churn como combinação linear das features passada por uma sigmoide, que comprime o resultado para [0, 1]. Aprende um peso por feature — interpretável: dá para ler quanto cada variável contribui. Regularização L2 (Ridge) leve contém overfitting sem zerar coeficientes.
2 · Random Forest
ensemble · baggingCem árvores de decisão treinadas em paralelo, cada uma sobre uma amostra aleatória dos dados e das features (bootstrap aggregating, ou bagging). A predição é o voto do conjunto. Árvores isoladas têm alta variância; agregar muitas a reduz sem aumentar o viés. Captura interações não-lineares e é robusta a outliers.
3 · Gradient Boosted Trees
ensemble · boosting
Também um conjunto de árvores, mas com estratégia oposta. O boosting é
sequencial: cada árvore corrige os erros residuais da anterior, descendo o
gradiente da função de perda. Ataca o viés, não a variância; costuma ser o
mais preciso, ao custo de mais treino e maior risco de overfitting se mal calibrado. O
stepSize (taxa de aprendizado) controla esse equilíbrio.
Bagging (Random Forest) treina árvores independentes e agrega — reduz a variância. Boosting (GBT) treina árvores em sequência, cada uma corrigindo a anterior — reduz o viés. Abordagens opostas para o mesmo fim: um conjunto melhor que qualquer árvore isolada.
Rastreamento com MLflow
Cada treino roda dentro de um mlflow.start_run(): parâmetros,
métricas e o modelo serializado são logados, e toda execução vira um run comparável na
interface. É a base do MLOps — rastreabilidade, comparação entre runs e nenhum
modelo sem proveniência.
def treinar_e_logar(modelo, params, nome_run): with mlflow.start_run(run_name=nome_run): m = modelo.fit(df_train) preds = m.transform(df_test) auc = evaluator_auc.evaluate(preds) # métrica de seleção f1 = evaluator_f1.evaluate(preds) mlflow.log_params(params) mlflow.log_metric("auc_roc", auc) mlflow.spark.log_model(m, "model") # modelo versionado return m, preds
Métrica de avaliação
O desbalanceamento determina a métrica. Com acurácia, um modelo que sempre previsse “não cancela” acertaria ~74% — inútil, pois erraria justamente os clientes relevantes. A métrica principal é a AUC-ROC: mede a capacidade de ordenar clientes por risco, independe do limiar de decisão e é robusta a classes desbalanceadas (0,5 = aleatório; 1,0 = separação perfeita). F1 e acurácia entram como secundárias.
A matriz de confusão traduz a métrica para o negócio: um falso negativo é um cliente que vai cancelar e não foi detectado — perda de receita; um falso positivo é um cliente fiel marcado como risco — custo de uma ação de retenção desnecessária. O peso entre os dois calibra o limiar.
Resultados
Treinados e avaliados no mesmo conjunto de teste, os três ficaram em empate técnico, todos na faixa de AUC 0,82. O melhor resultado, por margem mínima, foi do modelo mais simples:
| Modelo | Tipo | AUC-ROC | F1 | Acurácia | Status |
|---|---|---|---|---|---|
| Regressão Logística | Linear | 0,8259 | 0,7915 | 0,8006 | Selecionado |
| Random Forest | Bagging | 0,8253 | 0,7852 | 0,7984 | — |
| Gradient Boosted Trees | Boosting | 0,8220 | 0,7882 | 0,7963 | — |
Por que o modelo linear teve o melhor desempenho
Quando um modelo linear empata com Random Forest e GBT, a fronteira entre churn e retenção neste dataset é majoritariamente linear. Após o one-hot encoding, as variáveis mais preditivas (tipo de contrato, tenure, método de pagamento) separam bem as classes sem interações complexas, e os ensembles não encontraram estrutura não-linear relevante para explorar.
Pelo princípio da parcimônia, entre modelos de desempenho equivalente escolhe-se o mais simples. A Regressão Logística treina em segundos, é simples de servir e interpretável: a equipe de retenção lê por que um cliente foi marcado como risco. Para um caso de uso que orienta ações humanas, a interpretabilidade pesa mais do que 0,3% de AUC. Essa foi a lógica de seleção; o melhor AUC apenas confirmou a decisão.
ETAPA 06 — GOVERNANÇA & INFERÊNCIA
Governança e inferência
Um bom run no MLflow não equivale a aptidão para produção. O modelo selecionado é registrado no
Unity Catalog Model Registry — com uma signature que define o contrato
de entrada e saída — como portfolio.default.telco-churn-predictor,
versão 1. O registro separa a experimentação livre (qualquer run) da promoção governada (modelo
versionado, auditável, com rollback por alias).
Com o modelo promovido, a inferência em batch pontua a base de clientes, extrai a probabilidade de churn da classe positiva e a traduz em faixas acionáveis pela equipe de CRM:
| Faixa de risco | Probabilidade de churn | Ação recomendada |
|---|---|---|
| 🔴 ALTO | ≥ 0,70 | Retenção imediata — contato proativo, oferta dirigida |
| 🟡 MÉDIO | 0,40 – 0,70 | Monitoramento ativo, campanhas de relacionamento |
| 🟢 BAIXO | < 0,40 | Sem intervenção necessária |
O resultado é uma tabela Delta — telco_predictions — com um score e
uma faixa de risco por cliente, pronta para dashboards e sistemas downstream. Os limiares são
parâmetros de negócio, ajustáveis conforme o custo relativo entre perder um cliente e executar
uma ação de retenção desnecessária.
# Carrega o modelo promovido no Unity Catalog e pontua a base inteira model = mlflow.spark.load_model("models:/portfolio.default.telco-churn-predictor/1") df_preds = model.transform(df_gold_inference) # Extrai p(churn) do vetor de probabilidades e classifica por faixa extract_prob = F.udf(lambda v: float(v[1]), DoubleType()) df_scores = (df_preds .withColumn("churn_prob", extract_prob(F.col("probability"))) .withColumn("risco", F.when(F.col("churn_prob") >= 0.70, "ALTO") .when(F.col("churn_prob") >= 0.40, "MÉDIO") .otherwise("BAIXO"))) df_scores.write.format("delta").mode("overwrite").saveAsTable("portfolio.default.telco_predictions")
Considerações finais
O modelo selecionado tem AUC 0,826. Mas o resultado principal não é o modelo, e sim o sistema em torno dele: a arquitetura Medallion que mantém o dado confiável, o MLflow que torna cada experimento reproduzível, o Unity Catalog que governa a promoção e a inferência em batch que converte probabilidade em ação. Trocar o algoritmo é uma alteração pontual; a fundação é o que sustenta o pipeline.
Em resumo
- Aprendizado supervisionado — há rótulos (
Churn), logo é classificação binária, não clustering. - Três algoritmos comparados — Regressão Logística (linear), Random Forest (bagging) e GBT (boosting), todos distribuídos no Spark MLlib.
- Modelo selecionado — a Regressão Logística (AUC 0,8259) igualou os ensembles; parcimônia e interpretabilidade definiram a escolha.
- AUC-ROC como métrica — robusta ao desbalanceamento de ~26% de churn, ao contrário da acurácia.
- Engenharia de features decisiva — StringIndexer → OneHotEncoder → VectorAssembler, mais três features de domínio guiadas pela EDA.
- MLOps completo — Medallion + MLflow + Unity Catalog: dado confiável, experimentos rastreados, promoção governada, score por cliente.