Estudo de Caso: Da Recuperação de Crédito à Arquitetura Lakehouse
Transformação real em dados: de pipelines manuais e relatórios D-1 a uma arquitetura Lakehouse moderna, governada e automatizada com Databricks + Azure.

💡 Estudo de Caso: Da Recuperação de Crédito à Arquitetura Lakehouse
🎯 Contexto do Desafio
Empresa: Instituição financeira de médio porte com carteira de R$ 500M em crédito
Problema: Time de Recuperação de Crédito operando com dados fragmentados e decisões baseadas em relatórios desatualizados
Objetivo: Construir uma arquitetura moderna de dados para habilitar decisões em tempo real e previsibilidade de recuperação
🚨 O desafio real dos times de dados em Recuperação de Crédito
Cenário Inicial (Estado Atual)
Arquitetura Legada:
Sistema de Cobrança (Oracle)
↓ (ETL noturno manual)
CSV exportado para servidor FTP
↓ (Script Python local)
Banco SQL Server (Data Mart)
↓ (Power BI Import Mode)
Dashboard atualizado 1x/dia
Dores mapeadas:
1. Pipeline e Ingestão:
- ETLs batch com scripts Python executados via cron jobs sem orquestração
- Falhas silenciosas: quando o script quebra, ninguém é notificado até o analista perceber dados faltando
- Ausência de idempotência: reexecutar o pipeline gera dados duplicados
- Timeout em APIs de sistemas legados sem estratégia de retry exponencial
- Ingestão manual de 15+ fontes diferentes (JSON, CSV, XML, APIs REST)
2. Qualidade e Governança:
- Schema drift não tratado: quando o sistema fonte adiciona uma coluna, o pipeline quebra
- Mesmos clientes aparecendo com CPFs diferentes (formatação inconsistente)
- Impossível rastrear a origem de um dado específico (sem data lineage)
- Versionamento manual via sufixo de tabela (
tb_cobranca_v2,tb_cobranca_final) - Analistas e cientistas de dados trabalhando com cópias locais desatualizadas
3. Performance e Escalabilidade:
- Queries de agregação levando 40+ minutos em tabelas com 80M de registros
- Full table scan devido à ausência de particionamento inteligente
- Reprocessamento completo de histórico a cada atualização (não incremental)
- SQL Server saturado: 95% de CPU durante horário comercial
4. Colaboração e Governança:
- 3 versões diferentes da métrica "Taxa de Recuperação" (cada área calcula diferente)
- Cientistas de dados esperando 2 semanas para ter acesso a novas fontes de dados
- Ausência de ambientes de dev/staging: testes feitos direto em produção
- Zero automação: nenhum CI/CD para pipelines de dados
5. Impacto no Negócio:
- Decisões defasadas: gestores operando com dados de D-1 ou D-2
- Oportunidades perdidas: clientes em momento ideal de negociação não identificados a tempo
- Custo operacional alto: 60% do tempo do time de dados é gasto em "apagar incêndios"
🧱 Solução: Arquitetura Lakehouse Databricks + Azure
Proposta Técnica: Do Problema à Stack
| Desafio Técnico | Solução Implementada | Tecnologia |
|---|---|---|
| ETL manual e frágil | Pipeline declarativo com observabilidade | Delta Live Tables + Autoloader |
| Falhas silenciosas | Alertas automáticos e retry inteligente | Azure Monitor + DLT Expectations |
| Schema drift | Evolução automática de schema | Delta Lake schema evolution |
| Dados duplicados | Deduplicação por chave de negócio | Merge + UPSERT em Delta |
| Ausência de lineage | Rastreamento automático end-to-end | Unity Catalog + Azure Purview |
| Queries lentas | Processamento distribuído + otimizações | Spark + Photon + Z-Ordering |
| Métricas inconsistentes | Semântica única na Gold Layer | Databricks SQL Warehouse |
| Falta de CI/CD | Deploy automatizado e versionado | Git + Asset Bundles + GitHub Actions |
🔧 Implementação: Do Zero à Produção
Setup da Fundação
1.1 Arquitetura de Rede e Segurança
<details> <summary>📋 <strong>Código: Setup Azure Resources (Azure CLI)</strong></summary># Azure Resource Group e recursos core
az group create --name rg-lakehouse-prod --location brazilsouth
# Azure Data Lake Storage Gen2
az storage account create \
--name adlsrecuperacao \
--resource-group rg-lakehouse-prod \
--location brazilsouth \
--sku Standard_LRS \
--kind StorageV2 \
--hierarchical-namespace true
# Containers para camadas Medallion
az storage container create --name bronze --account-name adlsrecuperacao
az storage container create --name silver --account-name adlsrecuperacao
az storage container create --name gold --account-name adlsrecuperacao
az storage container create --name checkpoints --account-name adlsrecuperacao
1.2 Databricks Workspace e Unity Catalog
<details> <summary>📋 <strong>Código: Configuração Unity Catalog (Python SDK)</strong></summary># Configuração inicial do Unity Catalog via Databricks SDK
from databricks.sdk import WorkspaceClient
from databricks.sdk.service.catalog import *
w = WorkspaceClient()
# Criar Metastore (uma vez por região)
metastore = w.metastores.create(
name="metastore-brazil-south",
storage_root="abfss://unity-catalog@adlsrecuperacao.dfs.core.windows.net/",
region="brazilsouth"
)
# Criar Catalog para o projeto
catalog = w.catalogs.create(
name="recuperacao_credito",
comment="Dados de recuperação de crédito - Lakehouse Architecture"
)
# Criar Schemas (databases) para cada camada
w.schemas.create(
name="bronze",
catalog_name="recuperacao_credito",
comment="Raw data landing zone"
)
w.schemas.create(
name="silver",
catalog_name="recuperacao_credito",
comment="Cleaned and validated data"
)
w.schemas.create(
name="gold",
catalog_name="recuperacao_credito",
comment="Business-ready aggregated data"
)
1.3 Service Principal e RBAC
<details> <summary>📋 <strong>Código: Service Principal e Permissões (Azure CLI)</strong></summary># Criar Service Principal para CI/CD
az ad sp create-for-rbac \
--name "sp-databricks-cicd" \
--role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/rg-lakehouse-prod
# Atribuir permissões no ADLS (Storage Blob Data Contributor)
az role assignment create \
--assignee {service-principal-id} \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/{subscription-id}/resourceGroups/rg-lakehouse-prod/providers/Microsoft.Storage/storageAccounts/adlsrecuperacao
Implementação dos Pipelines
2.1 Bronze Layer: Ingestão Inteligente
Desafio: Ingerir 15 fontes diferentes (API REST, Oracle, CSV diários, Event Hub)
<details> <summary>📋 <strong>Código: Ingestão de Múltiplas Fontes (PySpark)</strong></summary># notebooks/bronze/ingest_sistema_cobranca.py
from pyspark.sql.functions import *
from pyspark.sql.types import *
# ===== FONTE 1: API REST do Sistema de Cobrança =====
# Problema: API retorna paginação, timeout frequente, rate limit de 100 req/min
def ingest_api_cobranca_incremental():
"""
Ingere dados da API com retry exponencial e checkpoint
"""
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Configurar retry strategy
retry_strategy = Retry(
total=5,
backoff_factor=2, # 1s, 2s, 4s, 8s, 16s
status_forcelist=[429, 500, 502, 503, 504]
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry_strategy))
# Ler último checkpoint (última data ingerida)
try:
last_run = spark.sql("""
SELECT MAX(data_extracao) as last_date
FROM recuperacao_credito.bronze.api_cobranca_raw
""").collect()[0]['last_date']
except:
last_run = "2024-01-01" # Data inicial se tabela não existe
# API endpoint com filtro incremental
base_url = "https://api-cobranca.empresa.com/v2/contratos"
params = {
"data_atualizacao_gte": last_run,
"page_size": 1000
}
all_records = []
page = 1
while True:
params['page'] = page
response = session.get(base_url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
if not data['results']:
break
all_records.extend(data['results'])
page += 1
# Rate limiting: 100 req/min = 1.6 req/s
time.sleep(0.6)
# Converter para DataFrame
schema = StructType([
StructField("id_contrato", StringType(), False),
StructField("cpf_cliente", StringType(), False),
StructField("valor_original", DecimalType(15,2), False),
StructField("valor_atualizado", DecimalType(15,2), False),
StructField("data_vencimento", DateType(), False),
StructField("status", StringType(), False),
StructField("data_atualizacao", TimestampType(), False)
])
df = spark.createDataFrame(all_records, schema=schema)
# Adicionar metadados de auditoria
df_bronze = df \
.withColumn("data_extracao", current_timestamp()) \
.withColumn("fonte", lit("api_sistema_cobranca")) \
.withColumn("versao_api", lit("v2"))
# Escrita idempotente com MERGE (evita duplicatas em reprocessamento)
df_bronze.createOrReplaceTempView("bronze_temp")
spark.sql("""
MERGE INTO recuperacao_credito.bronze.api_cobranca_raw AS target
USING bronze_temp AS source
ON target.id_contrato = source.id_contrato
AND target.data_atualizacao = source.data_atualizacao
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *
""")
# ===== FONTE 2: Arquivos CSV diários via SFTP =====
# Problema: Schema muda sem aviso, arquivos corrompidos ocasionalmente
# Configuração do Autoloader para detecção automática
autoloader_config = {
"cloudFiles.format": "csv",
"cloudFiles.schemaLocation": "abfss://checkpoints@adlsrecuperacao.dfs.core.windows.net/schema/pagamentos",
"cloudFiles.inferColumnTypes": "true",
"cloudFiles.schemaEvolutionMode": "addNewColumns", # Permite novas colunas automaticamente
"cloudFiles.schemaHints": "valor_pago DECIMAL(15,2), data_pagamento DATE",
"cloudFiles.maxFilesPerTrigger": 100,
"cloudFiles.useNotifications": "true", # Azure Event Grid para eficiência
# Tratamento de arquivos corrompidos
"badRecordsPath": "abfss://bronze@adlsrecuperacao.dfs.core.windows.net/bad_records/pagamentos",
"mode": "PERMISSIVE" # Linha com erro vira null, mas não quebra pipeline
}
df_pagamentos = (spark.readStream
.format("cloudFiles")
.options(**autoloader_config)
.option("header", "true")
.option("delimiter", ";")
.option("encoding", "ISO-8859-1") # Encoding comum no Brasil
.load("abfss://landing@adlsrecuperacao.dfs.core.windows.net/pagamentos/")
.withColumn("arquivo_origem", input_file_name())
.withColumn("data_ingestao", current_timestamp())
)
# Escrita com checkpoint para garantir exactly-once semantics
query = (df_pagamentos.writeStream
.format("delta")
.outputMode("append")
.option("checkpointLocation", "abfss://checkpoints@adlsrecuperacao.dfs.core.windows.net/bronze_pagamentos")
.option("mergeSchema", "true")
.trigger(availableNow=True) # Processa todos disponíveis e para
.table("recuperacao_credito.bronze.pagamentos_csv")
)
query.awaitTermination()
# ===== FONTE 3: Change Data Capture do Oracle =====
# Problema: Volume alto (1M+ rows/dia), necessidade de streaming
from pyspark.sql.functions import from_json, col
# Schema do CDC event do Oracle
cdc_schema = StructType([
StructField("operation", StringType()), # INSERT, UPDATE, DELETE
StructField("timestamp", TimestampType()),
StructField("table", StringType()),
StructField("before", StringType()), # JSON do registro anterior
StructField("after", StringType()) # JSON do registro novo
])
# Ler do Azure Event Hub (conectado ao Oracle via Debezium)
df_cdc = (spark.readStream
.format("eventhubs")
.option("eventhubs.connectionString", dbutils.secrets.get("key-vault", "eventhub-connection"))
.option("eventhubs.consumerGroup", "databricks-bronze")
.option("maxEventsPerTrigger", 10000)
.load()
.select(from_json(col("body").cast("string"), cdc_schema).alias("data"))
.select("data.*")
.withColumn("data_ingestao_streaming", current_timestamp())
)
# Escrita em modo streaming (low latency)
(df_cdc.writeStream
.format("delta")
.outputMode("append")
.option("checkpointLocation", "abfss://checkpoints@adlsrecuperacao.dfs.core.windows.net/bronze_cdc_oracle")
.trigger(processingTime="30 seconds") # Micro-batch a cada 30s
.table("recuperacao_credito.bronze.cdc_oracle_clientes")
.start()
)
Melhorias implementadas na Bronze:
- ✅ Idempotência via MERGE (reprocessamento seguro)
- ✅ Schema evolution automático (novas colunas não quebram pipeline)
- ✅ Bad records handling (linhas com erro isoladas, não param pipeline)
- ✅ Checkpoint para exactly-once guarantee
- ✅ Retry exponencial para APIs instáveis
- ✅ Rate limiting para respeitar limites de API
2.2 Silver Layer: Transformação com Qualidade
Desafio: Dados vêm com CPF em formatos diferentes, duplicatas, datas inválidas
<details> <summary>📋 <strong>Código: Transformação e Quality Checks (Delta Live Tables)</strong></summary># notebooks/silver/transform_cobranca_clean.py
import dlt
from pyspark.sql.functions import *
from pyspark.sql.types import *
# ===== LIMPEZA E PADRONIZAÇÃO =====
@dlt.table(
name="cobranca_cleaned",
comment="Dados de cobrança limpos, deduplicados e validados",
table_properties={
"quality": "silver",
"pipelines.autoOptimize.zOrderCols": "cpf_clean,data_vencimento"
}
)
@dlt.expect_or_drop("cpf_valido", "length(cpf_clean) = 11 AND cpf_clean RLIKE '^[0-9]{11}$'")
@dlt.expect_or_drop("valor_positivo", "valor_divida_atualizado > 0")
@dlt.expect_or_drop("data_vencimento_valida", "data_vencimento >= '2020-01-01' AND data_vencimento <= current_date()")
@dlt.expect_or_fail("chave_unica", "id_contrato IS NOT NULL") # Faz pipeline falhar se chave nula
def cobranca_cleaned():
# UDF para validação de CPF (algoritmo oficial)
@udf(returnType=BooleanType())
def validar_cpf(cpf):
if not cpf or len(cpf) != 11:
return False
# Verifica se não é sequência repetida (111.111.111-11)
if cpf == cpf[0] * 11:
return False
# Validação dos dígitos verificadores
def calcular_digito(cpf_parcial, peso_inicial):
soma = sum(int(cpf_parcial[i]) * (peso_inicial - i) for i in range(len(cpf_parcial)))
resto = soma % 11
return 0 if resto < 2 else 11 - resto
digito1 = calcular_digito(cpf[:9], 10)
digito2 = calcular_digito(cpf[:10], 11)
return cpf[-2:] == f"{digito1}{digito2}"
return (
dlt.read_stream("recuperacao_credito.bronze.api_cobranca_raw")
# Limpeza de CPF: remove pontuação e valida
.withColumn("cpf_clean", regexp_replace(col("cpf_cliente"), r'[.\-/]', ''))
.withColumn("cpf_valido_flag", validar_cpf(col("cpf_clean")))
# Padronização de valores monetários
.withColumn("valor_divida_atualizado",
when(col("valor_atualizado").isNull(), col("valor_original"))
.otherwise(col("valor_atualizado")))
# Cálculo de aging (dias em atraso)
.withColumn("dias_atraso",
datediff(current_date(), col("data_vencimento")))
# Classificação de risco baseada em aging
.withColumn("faixa_risco",
when(col("dias_atraso") <= 30, "BAIXO")
.when(col("dias_atraso") <= 90, "MEDIO")
.when(col("dias_atraso") <= 180, "ALTO")
.otherwise("CRITICO"))
# Deduplicação: em caso de múltiplos registros, pega o mais recente
.withColumn("row_num",
row_number().over(
Window.partitionBy("id_contrato")
.orderBy(col("data_atualizacao").desc())
))
.filter(col("row_num") == 1)
.drop("row_num")
# Metadados de processamento
.withColumn("processado_em", current_timestamp())
.withColumn("versao_pipeline", lit("v2.1"))
)
# ===== ENRIQUECIMENTO COM DIMENSÕES =====
@dlt.table(
name="cobranca_enriched",
comment="Dados de cobrança enriquecidos com dados do cliente e localização"
)
@dlt.expect_or_drop("cliente_existe", "id_cliente IS NOT NULL")
def cobranca_enriched():
df_cobranca = dlt.read("cobranca_cleaned")
# Dimensão de clientes (SCD Type 2 - histórico de mudanças)
df_clientes = dlt.read("recuperacao_credito.silver.dim_clientes")
# Dimensão de localização (CEP -> Cidade/Estado/Região)
df_localizacao = dlt.read("recuperacao_credito.silver.dim_localizacao")
return (
df_cobranca
# Join com dimensão de clientes
.join(
df_clientes.filter(col("is_current") == True), # Apenas registros atuais do SCD
df_cobranca.cpf_clean == df_clientes.cpf,
"left"
)
# Join com localização
.join(
df_localizacao,
df_clientes.cep == df_localizacao.cep,
"left"
)
# Selecionar colunas finais
.select(
# Chaves
col("id_contrato"),
col("cpf_clean").alias("cpf"),
df_clientes.id_cliente,
# Atributos de cobrança
col("valor_divida_atualizado"),
col("data_vencimento"),
col("dias_atraso"),
col("faixa_risco"),
col("status"),
# Atributos do cliente
df_clientes.nome_cliente,
df_clientes.data_nascimento,
df_clientes.score_credito,
df_clientes.renda_estimada,
df_clientes.segmento, # Varejo, Agro, Empresarial
# Atributos de localização
df_localizacao.cidade,
df_localizacao.estado,
df_localizacao.regiao, # Norte, Nordeste, Sul, Sudeste, Centro-Oeste
df_localizacao.tipo_cidade, # Capital, Interior, Região Metropolitana
# Metadados
col("processado_em"),
col("versao_pipeline")
)
)
# ===== QUALITY METRICS (Monitoramento de Qualidade) =====
@dlt.table(
name="quality_metrics_cobranca",
comment="Métricas de qualidade dos dados de cobrança"
)
def quality_metrics():
df = dlt.read("cobranca_cleaned")
return df.agg(
count("*").alias("total_registros"),
# Completude
(sum(when(col("cpf_clean").isNull(), 1).otherwise(0)) / count("*") * 100).alias("pct_cpf_nulo"),
(sum(when(col("data_vencimento").isNull(), 1).otherwise(0)) / count("*") * 100).alias("pct_data_nula"),
# Validade
(sum(when(col("cpf_valido_flag") == False, 1).otherwise(0)) / count("*") * 100).alias("pct_cpf_invalido"),
(sum(when(col("valor_divida_atualizado") <= 0, 1).otherwise(0)) / count("*") * 100).alias("pct_valor_invalido"),
# Unicidade
(count("*") - countDistinct("id_contrato")).alias("total_duplicatas"),
# Estatísticas descritivas
avg("valor_divida_atualizado").alias("valor_medio"),
stddev("valor_divida_atualizado").alias("valor_desvio_padrao"),
percentile_approx("dias_atraso", 0.5).alias("dias_atraso_mediana"),
# Timestamp de cálculo
current_timestamp().alias("calculado_em")
)
Melhorias implementadas na Silver:
- ✅ Data Quality Checks declarativos (
@dlt.expect) - ✅ Validação de CPF com algoritmo oficial
- ✅ Deduplicação inteligente (mais recente ganha)
- ✅ Enriquecimento via joins com dimensões
- ✅ Quality Metrics para observabilidade
- ✅ SCD Type 2 para histórico de mudanças em clientes
2.3 Gold Layer: KPIs de Negócio
Desafio: Cada área calculava "Taxa de Recuperação" de forma diferente
<details> <summary>📋 <strong>Código: KPIs e Análise de Tendências (Delta Live Tables)</strong></summary># notebooks/gold/kpis_recuperacao.py
import dlt
from pyspark.sql.functions import *
from pyspark.sql.window import Window
@dlt.table(
name="kpi_recuperacao_mensal",
comment="KPIs padronizados de recuperação de crédito - atualização D+1",
table_properties={
"quality": "gold",
"pipelines.autoOptimize.zOrderCols": "ano_mes,regiao",
"delta.enableChangeDataFeed": "true" # Habilita CDC para downstream
}
)
def kpi_recuperacao_mensal():
df_cobranca = dlt.read("recuperacao_credito.silver.cobranca_enriched")
df_pagamentos = dlt.read("recuperacao_credito.silver.pagamentos_enriched")
# Definição padronizada de "Recuperação" (acordo entre áreas):
# - Contrato com status PAGO ou ACORDO_ATIVO
# - Pagamento recebido > 0 nos últimos 30 dias
df_base = (
df_cobranca
.join(df_pagamentos, "id_contrato", "left")
.withColumn("ano_mes", date_format(col("data_vencimento"), "yyyy-MM"))
.withColumn("ano", year(col("data_vencimento")))
.withColumn("mes", month(col("data_vencimento")))
)
return (
df_base
.groupBy("ano_mes", "ano", "mes", "regiao", "segmento", "faixa_risco")
.agg(
# === VOLUME ===
count("id_contrato").alias("total_contratos"),
countDistinct("cpf").alias("total_clientes_unicos"),
# === VALORES ===
sum("valor_divida_atualizado").alias("valor_total_carteira"),
sum(when(col("status").isin("PAGO", "ACORDO_ATIVO"),
col("valor_pago")).otherwise(0)).alias("valor_recuperado"),
# === TAXAS (KPIs principais) ===
# Taxa de Recuperação = (Contratos Recuperados / Total Contratos) * 100
(sum(when(col("status").isin("PAGO", "ACORDO_ATIVO"), 1).otherwise(0))
/ count("*") * 100).alias("taxa_recuperacao_contratos_pct"),
# Taxa de Recuperação Financeira = (Valor Recuperado / Valor Total) * 100
(sum(when(col("status").isin("PAGO", "ACORDO_ATIVO"), col("valor_pago")).otherwise(0))
/ sum("valor_divida_atualizado") * 100).alias("taxa_recuperacao_financeira_pct"),
# === AGING ===
avg("dias_atraso").alias("dias_atraso_medio"),
percentile_approx("dias_atraso", 0.5).alias("dias_atraso_mediana"),
max("dias_atraso").alias("dias_atraso_maximo"),
# === DISTRIBUIÇÃO POR FAIXA ===
sum(when(col("faixa_risco") == "BAIXO", 1).otherwise(0)).alias("contratos_risco_baixo"),
sum(when(col("faixa_risco") == "MEDIO", 1).otherwise(0)).alias("contratos_risco_medio"),
sum(when(col("faixa_risco") == "ALTO", 1).otherwise(0)).alias("contratos_risco_alto"),
sum(when(col("faixa_risco") == "CRITICO", 1).otherwise(0)).alias("contratos_risco_critico"),
# === METADADOS ===
current_timestamp().alias("atualizado_em"),
lit("v1.0").alias("versao_metrica")
)
)
# ===== ANÁLISE DE TENDÊNCIA (MoM e YoY) =====
@dlt.table(
name="kpi_recuperacao_tendencias",
comment="Análise de tendências com comparação mês a mês e ano a ano"
)
def kpi_recuperacao_tendencias():
df_kpi = dlt.read("kpi_recuperacao_mensal")
# Window specs para cálculos de tendência
window_mom = Window.partitionBy("regiao", "segmento").orderBy("ano_mes")
window_yoy = Window.partitionBy("regiao", "segmento", "mes").orderBy("ano")
return (
df_kpi
# Valores do mês anterior (MoM)
.withColumn("taxa_recuperacao_mes_anterior",
lag("taxa_recuperacao_contratos_pct", 1).over(window_mom))
.withColumn("variacao_mom_pct",
((col("taxa_recuperacao_contratos_pct") - col("taxa_recuperacao_mes_anterior"))
/ col("taxa_recuperacao_mes_anterior") * 100))
# Valores do mesmo mês no ano anterior (YoY)
.withColumn("taxa_recuperacao_ano_anterior",
lag("taxa_recuperacao_contratos_pct", 1).over(window_yoy))
.withColumn("variacao_yoy_pct",
((col("taxa_recuperacao_contratos_pct") - col("taxa_recuperacao_ano_anterior"))
/ col("taxa_recuperacao_ano_anterior") * 100))
# Classificação de performance
.withColumn("classificacao_performance",
when(col("variacao_mom_pct") > 10, "EXCELENTE")
.when(col("variacao_mom_pct") > 0, "BOM")
.when(col("variacao_mom_pct") > -10, "NEUTRO")
.otherwise("CRÍTICO"))
# Médias móveis (MA - Moving Average)
.withColumn("taxa_recuperacao_ma3", # Média dos últimos 3 meses
avg("taxa_recuperacao_contratos_pct").over(
window_mom.rowsBetween(-2, 0)))
.withColumn("taxa_recuperacao_ma6", # Média dos últimos 6 meses
avg("taxa_recuperacao_contratos_pct").over(
window_mom.rowsBetween(-5, 0)))
)
Melhorias implementadas na Gold:
- ✅ KPIs padronizados (acordo entre áreas)
- ✅ Análise de tendências (MoM, YoY, médias móveis)
- ✅ Change Data Feed habilitado para auditoria
- ✅ Z-Ordering em colunas de filtro frequente
Otimização e Governança
3.1 Otimizações de Performance
<details> <summary>📋 <strong>Código: Otimizações Delta Lake (SQL)</strong></summary>-- ===== VACUUM: Limpeza de versões antigas =====
VACUUM recuperacao_credito.silver.cobranca_enriched RETAIN 168 HOURS;
-- ===== Z-ORDERING: Colocação de dados relacionados =====
OPTIMIZE recuperacao_credito.gold.kpi_recuperacao_mensal
ZORDER BY (ano_mes, regiao, segmento);
-- ===== LIQUID CLUSTERING: Alternativa moderna ao Z-Ordering =====
ALTER TABLE recuperacao_credito.gold.kpi_recuperacao_mensal
CLUSTER BY (ano_mes, regiao);
-- ===== TIME TRAVEL: Audit e recuperação =====
SELECT * FROM recuperacao_credito.gold.kpi_recuperacao_mensal VERSION AS OF 42;
3.2 Governança com Unity Catalog
<details> <summary>📋 <strong>Código: Controle de Acesso e Mascaramento (SQL)</strong></summary>-- ===== CONTROLE DE ACESSO GRANULAR =====
CREATE GROUP IF NOT EXISTS analistas_recuperacao;
CREATE GROUP IF NOT EXISTS cientistas_dados;
CREATE GROUP IF NOT EXISTS engenheiros_dados;
-- Permissões em nível de Schema
GRANT SELECT ON SCHEMA recuperacao_credito.gold TO GROUP analistas_recuperacao;
GRANT SELECT ON SCHEMA recuperacao_credito.silver TO GROUP cientistas_dados;
GRANT ALL PRIVILEGES ON CATALOG recuperacao_credito TO GROUP engenheiros_dados;
-- ===== MASCARAMENTO DE DADOS SENSÍVEIS =====
CREATE FUNCTION recuperacao_credito.mask_cpf(cpf STRING)
RETURNS STRING
RETURN CASE
WHEN is_member('gestores_c_level') THEN cpf
ELSE CONCAT(SUBSTRING(cpf, 1, 3), '*****', SUBSTRING(cpf, 9, 3))
END;
-- ===== AUDIT LOG =====
SELECT
user_identity.email,
event_time,
action_name,
request_params.full_name_arg AS tabela_acessada
FROM system.access.audit
WHERE action_name IN ('getTable', 'commandSubmit')
AND request_params.full_name_arg LIKE 'recuperacao_credito.gold%'
AND event_date >= current_date() - INTERVAL 7 DAYS
ORDER BY event_time DESC;
CI/CD e Automação
4.1 Asset Bundle Configuration
<details> <summary>📋 <strong>Código: Databricks Asset Bundle (YAML)</strong></summary># databricks.yml
bundle:
name: lakehouse-recuperacao-credito
environments:
dev:
default: true
workspace:
host: https://adb-123456789.azuredatabricks.net
variables:
catalog_name: "recuperacao_credito_dev"
prod:
workspace:
host: https://adb-111222333.azuredatabricks.net
variables:
catalog_name: "recuperacao_credito"
resources:
pipelines:
silver_pipeline:
name: "[${bundle.environment}] Silver - Cobrança e Clientes"
target: "${var.catalog_name}.silver"
libraries:
- notebook:
path: ./notebooks/silver/transform_cobranca_clean.py
clusters:
- label: "default"
node_type_id: "Standard_DS3_v2"
autoscale:
min_workers: 2
max_workers: 8
continuous: false
notifications:
- email_recipients:
- engenharia-dados@empresa.com
on_failure: true
jobs:
daily_ingestion:
name: "[${bundle.environment}] Ingestão Diária - Cobrança"
tasks:
- task_key: ingest_bronze
notebook_task:
notebook_path: ./notebooks/bronze/ingest_api_cobranca.py
job_cluster_key: "shared_cluster"
- task_key: run_silver_pipeline
depends_on:
- task_key: ingest_bronze
pipeline_task:
pipeline_id: ${resources.pipelines.silver_pipeline.id}
schedule:
quartz_cron_expression: "0 0 6 * * ?"
timezone_id: "America/Sao_Paulo"
4.2 GitHub Actions Workflow
<details> <summary>📋 <strong>Código: CI/CD Pipeline (GitHub Actions)</strong></summary># .github/workflows/deploy-prod.yml
name: Deploy to Production
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Run unit tests
run: pytest tests/unit/ -v --cov=notebooks
deploy:
runs-on: ubuntu-latest
needs: test
environment: production
steps:
- uses: actions/checkout@v3
- name: Deploy bundle
run: databricks bundle deploy -e prod
- name: Notify Slack
if: success()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d '{"text":"✅ Deploy em PRODUÇÃO concluído!"}'
📊 Resultados Mensuráveis
Métricas Técnicas (Antes vs Depois)
| Métrica | Antes | Depois | Melhoria |
|---|---|---|---|
| Tempo de ingestão | 4h (batch ETL manual) | 15min (Autoloader incremental) | ⬇️ 93% |
| Latência de dados (Bronze → Gold) | D+1 (diário às 6h) | 45min (quase real-time) | ⬇️ 96% |
| Taxa de falha em pipelines | ~15% (retry manual) | <2% (retry automático + DLT) | ⬆️ 87% confiabilidade |
| Tempo para auditoria (data lineage) | 2-3 dias (busca manual) | 5 minutos (Unity Catalog UI) | ⬇️ 99% |
| Queries em tabelas grandes (80M rows) | 40min (SQL Server) | 3min (Delta + Photon) | ⬇️ 92% |
| Custo de storage | $8.5k/mês (SQL Server Premium) | $2.1k/mês (ADLS + Delta) | ⬇️ 75% |
| Tempo para deploy de pipeline novo | 2 semanas (manual) | 2 dias (CI/CD) | ⬇️ 85% |
| Reprocessamento de histórico | 8h (full refresh) | 20min (incremental + Time Travel) | ⬇️ 95% |
Métricas de Negócio
| KPI | Impacto |
|---|---|
| Time-to-Insight | Gestores tomam decisões com dados de última hora vs D-1 |
| Taxa de Recuperação | Aumento de 12% em 6 meses (identificação precoce de oportunidades) |
| Produtividade do Time de Dados | 60% menos tempo em "firefighting", mais tempo em análises estratégicas |
| Qualidade de Dados | <0.5% de erro em métricas (antes: ~8% de divergência entre áreas) |
| Compliance (LGPD) | 100% de rastreabilidade de PII + mascaramento automático |
📊 Arquitetura de Referência Completa
┌─────────────────────────────────────────────────────────────────┐
│ FONTES DE DADOS │
│ • APIs REST (sistemas de cobrança) │
│ • Bancos SQL (Oracle, SQL Server) │
│ • Arquivos (CSV, JSON, Parquet) │
│ • Event Streaming (Azure Event Hub) │
└─────────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ INGESTÃO (Azure Data Factory) │
│ • Copy Activity para batch │
│ • Event Hub Trigger para streaming │
│ • Autoloader para file ingestion │
└─────────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ BRONZE LAYER (ADLS Gen2 + Delta Lake) │
│ • Dados brutos preservados │
│ • Schema evolution habilitado │
│ • Auditoria: timestamp + source file │
└─────────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SILVER LAYER (Delta Live Tables + Spark Structured) │
│ • Limpeza e deduplicação │
│ • Validações de qualidade (@dlt.expect) │
│ • Enriquecimento (joins com dimensões) │
└─────────────────────┬───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ GOLD LAYER (Databricks SQL Warehouse) │
│ • Agregações e KPIs │
│ • Otimizações: Z-Order, Data Skipping │
│ • Semantic Layer para BI │
└─────────────────────┬───────────────────────────────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ POWER BI │ │ ML/AI │
│ (Direct Lake) │ │ • MLflow │
│ • Dashboards │ │ • Feature Store │
│ • Alerts │ │ • Predição │
└──────────────────┘ └──────────────────┘
GOVERNANÇA (Unity Catalog + Azure Purview)
• Data Lineage end-to-end
• Access Control (RBAC/ABAC)
• PII Classification
💡 Lições Aprendidas
✅ O que funcionou bem
- Começar pequeno: implementamos 1 pipeline crítico (API de Cobrança) antes de escalar
- Delta Live Tables: redução dramática de código (50% menos linhas vs Spark tradicional)
- Unity Catalog desde o início: evitou retrabalho de governança
- Autoloader: detecção automática de schema salvou inúmeros "bugs de produção"
⚠️ Desafios enfrentados
- Curva de aprendizado: time levou ~3 semanas para dominar DLT + Unity Catalog
- Migração de dados históricos: 5 anos de dados legados (2TB) levou 1 semana
- Integração com Oracle legado: CDC com Debezium exigiu ajustes no DBA
- Mudança cultural: analistas resistentes a "não mais ter Excel local"
🔧 Próximos passos
- Machine Learning: Modelo de propensão a pagamento (MLflow)
- Real-time scoring: API para sugerir melhor horário de contato
- Data Quality Dashboard: Monitoramento proativo de anomalias
- Expansão: Aplicar arquitetura para outras áreas (Originação, Crédito)
📚 Recursos de Estudo Recomendados
Para Iniciantes em Databricks
- Curso Gratuito: Databricks Academy - Lakehouse Fundamentals
- Documentação: Delta Lake - Quick Start
- Prática: Databricks Community Edition (grátis)
Para Intermediários
- Certificação: Databricks Data Engineer Associate
- Hands-on: Delta Live Tables Workshop
- Azure: DP-203 - Data Engineering on Azure
Comunidade e Suporte
Conceitos-chave para dominar
Fundamentos:
- ACID Transactions
- Idempotência em pipelines
- Schema Evolution
- Particionamento (Hive-style vs Liquid Clustering)
Avançado:
- Photon Engine internals
- Delta Lake transaction log
- Structured Streaming micro-batching
- Unity Catalog privilege model
💬 Conclusão
Este estudo de caso demonstra que a transição para Arquitetura Lakehouse não é apenas uma mudança de ferramentas, é uma transformação em como tratamos dados como ativos estratégicos.
Principais Takeaways:
- Comece incremental: 1-2 pipelines críticos, valide, depois escale
- Governança desde o dia 1: Unity Catalog evita débito técnico futuro
- Automação é essencial: CI/CD reduz erros humanos e acelera entregas
- Monitore qualidade:
@dlt.expectprevine dados ruins em produção - Otimize continuamente: Z-Order, Vacuum e Liquid Clustering fazem diferença real
Para times de Recuperação de Crédito, essa arquitetura resolve problemas críticos:
- ✅ Performance via processamento distribuído (Spark + Photon)
- ✅ Confiabilidade via ACID transactions e retry automático
- ✅ Governança via lineage e controle de acesso granular
- ✅ Escalabilidade via arquitetura desacoplada e cloud-native
O diferencial está na disciplina de engenharia: testes automatizados, versionamento, observabilidade e melhoria contínua.
A jornada para Lakehouse é iterativa, não big-bang, e os resultados (93% redução no tempo de ingestão, 12% aumento na taxa de recuperação) comprovam o valor dessa abordagem.
✍️ Por Alexsander Valente
Engenheiro de Dados & IA
🏗️ Databricks • Spark • LangChain • Azure • AWS
📎 linkedin.com/in/alexsander-valente
📧 Dúvidas ou quer discutir implementação? Entre em contato!
#DataEngineering #Lakehouse #Databricks #DeltaLake #Azure #RecuperacaoDeCredito #DataGovernance #MLOps #EstudoDeCaso


