- Python
- pandas
- matplotlib
- seaborn
- scikit-learn
- XGBoost
- Machine Learning
Análise de Churn de Clientes
Estudo de churn de clientes de um banco, da análise exploratória à modelagem e a um preditor interativo de evasão.
Estudo de churn de clientes de um banco, feito em dupla com meu amigo Juan Braz sobre a base pública “Bank Customer Churn” do Kaggle (10.000 clientes e 18 colunas). O trabalho virou uma série de três artigos no Medium, em inglês: eu fiquei com a análise exploratória (Parte 1, no meu perfil skicklel) e o Juan com a modelagem e validação (Parte 2) e com o preditor interativo (Parte 3). O objetivo era entender quais características separam quem sai de quem fica e, na sequência, prever a evasão.
O que foi investigado
Na parte exploratória eu comparei cada variável com o alvo Exited (se o cliente saiu ou ficou). A base não tinha valores nulos nem duplicados, então a limpeza foi conferir tipos e partir para as visualizações.
Os padrões mais claros:
- A faixa de 28 a 42 anos é a mais ativa; dentro dela, quem tem cerca de 35 anos tende a ficar e quem tem 40 tende a sair. Entre 49 e 57 anos a saída também é mais comum, enquanto 18 a 27 e 66 a 81 saem muito pouco.
- Mulheres saem proporcionalmente mais que homens.
- Por nacionalidade, clientes da França ficam mais, os da Alemanha saem mais e os da Espanha ficam no meio.
- Quem não tem cartão de crédito sai mais que quem tem; o tipo de cartão (Silver, Gold, Platinum, Diamond) não mostrou correlação relevante.
- Clientes inativos saem mais, ativos ficam.
- Saldo baixo se associa a ficar, enquanto saldo entre US$ 120 mil e US$ 150 mil se associa a sair.
Na matriz de correlação, as variáveis com correlação mais forte com Exited foram Age, Balance e IsActiveMember. Essa matriz usou só colunas numéricas, por isso o Complain, que é o grande vazamento da base, não apareceu aí.
Pra ler cada categoria, eu plotava a contagem dividida por quem saiu e quem ficou e anotava o percentual de cada grupo sobre a barra:
for coluna in ['Card Type', 'HasCrCard']:
plt.figure(figsize=(10, 6))
ax = sns.countplot(x=df_bank[coluna], hue=df_bank['Exited'],
palette="viridis", dodge=True)
for container in ax.containers:
total = sum(p.get_height() for p in container)
for p in container:
height = p.get_height()
percentage = f'{100 * height / total:.1f}%' if total > 0 else '0.0%'
ax.annotate(percentage, (p.get_x() + p.get_width() / 2., height),
ha='center', va='bottom', fontsize=10, color='black')
plt.title(f'Distribuição de Churn por {coluna}')
Modelagem e validação
Na Parte 2 o Juan seguiu com o pipeline clássico: one-hot encoding nas categóricas, StandardScaler nas numéricas, split estratificado de 80/20 e três modelos (Regressão Logística, Random Forest e XGBoost), comparados por acurácia, AUC, precisão, recall e F1.
O split e o encoding do notebook:
X = df_bank.drop('Exited', axis=1) # features
y = df_bank['Exited'] # alvo
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y)
X_train_encoded = pd.get_dummies(X_train)
X_test_encoded = pd.get_dummies(X_test)
X_train_encoded, X_test_encoded = X_train_encoded.align(
X_test_encoded, join='left', axis=1, fill_value=0)
Os três modelos ficaram com acurácia acima de 99,7% no teste, AUC entre 0,997 e 0,9996 e F1 acima de 0,99, e a validação cruzada de 5 folds repetiu o padrão (sempre perto de 0,998). Numa base de churn com só ~20% de saída, um resultado tão perfeito deveria ter sido o primeiro sinal de que havia algo errado nos dados, mas na época tratamos como dado “bem preparado” e até concluímos que não havia vazamento. A Parte 3 fecha com um script que coleta os dados de um cliente via input() e devolve a probabilidade de saída de cada modelo.
Particularidades e pontos que revisaria
-
Vazamento de informação embutido na base. As colunas
ComplaineSatisfaction Scoresão registradas depois da reclamação e do atendimento, então não existem antes do cliente decidir sair, e a primeira é praticamente o gabarito do alvo:Complaintem média 0,2044 enquantoExitedtem 0,2038, quase a mesma contagem. Com isso os modelos “aprendem” a responder pela coluna vazada e batem ~99,8%. Hoje eu revisitaria a conclusão dos artigos: era o próprio dado do Kaggle vazando, não mérito do pipeline. -
Preditor interativo herda o vazamento. A ferramenta da Parte 3 pede
Complain,Satisfaction ScoreePoints Earnedcomo entrada. Num cenário real essas informações só existem depois do evento que se quer prever, então o “preditor em tempo real” replica o mesmo problema da base. -
Dois pipelines de pré-processamento no notebook. Uma célula monta um
df_encoded(one-hot comdrop_first=True+StandardScaler) e anuncia que os dados estão prontos para modelagem, mas a modelagem logo depois ignora esse dataframe, refaz o encoding já sobreX_train/X_testcomalign. Sobrou código morto e duas formas diferentes de preparar os mesmos dados. -
Colunas de identificação entram como features. O
Xé montado só comdf_bank.drop('Exited'), entãoRowNumber,CustomerIdeSurnameseguem no modelo. Pior:Surnameéobject, e opd.get_dummies(X_train)sem filtrar transforma isso em milhares de colunas dummy inúteis. -
“Validação externa” sobre dados sintéticos aleatórios. A base externa foi gerada com
np.random.choice, sorteandoExitedeComplainde forma independente; numa base assim as features não têm relação com o alvo, então reportar AUC perto de 1 nela é implausível. Essa etapa eu revalidaria, porque o resultado não confere com o que uma base puramente aleatória deveria produzir. -
Repositório pesado e sem
.gitignore. O CSV de 837 KB e omain.ipynbcom todos os outputs e imagens em base64 (uns 1,4 MB) foram commitados; um notebook limpo ficaria bem menor e mais fácil de revisar.
Pontos de melhoria
- Remover as colunas pós-evento (
Complain,Satisfaction Score,Points Earned) das features, ou apresentar duas versões do estudo (uma com e uma sem) para separar o exercício acadêmico das métricas honestas. - Tirar
RowNumber,CustomerIdeSurnamedoXantes de qualquer encoding. - Unificar o pré-processamento num único
Pipeline/ColumnTransformere apagar as células mortas do notebook. - Trocar a validação externa por um conjunto sintético que preserve as correlações reais, ou por uma fatia temporal dos próprios dados, respeitando a ordem no tempo (features medidas antes do rótulo).
- Corrigir a ferramenta interativa para pedir só features disponíveis no momento da consulta, ou documentar a limitação.
- Como o churn tem só ~20% de positivos, avaliar métricas de negócio (lift/gain) e tratar o desbalanceamento em vez de confiar só na acurácia.
- Limpar os outputs do notebook e pôr o CSV e os
.ipynbpesados no.gitignore.
Gabriel Rocha