Qué hicimos, por qué
y qué nos dicen los resultados
Interpretación técnica y detallada del notebook completo. Cada decisión, cada salida, cada número explicado desde cero.
El punto de partida es un problema de negocio concreto: un banco quiere saber qué clientes están en riesgo de abandonar su servicio antes de que lo hagan. Esto es lo que se llama predicción de churn, y tiene un valor económico directo: retener a un cliente existente es mucho más barato que captar uno nuevo.
Para resolverlo usamos un algoritmo de Machine Learning llamado SVM (Support Vector Machine). La idea conceptual es la siguiente: cada cliente es un punto en un espacio multidimensional donde cada dimensión es una de sus características — su edad, su saldo, su país, si es miembro activo… El SVM busca la mejor frontera posible que separe los puntos «se va» de los puntos «se queda».
Imagina que tienes una mesa con bolitas rojas (clientes que se fueron) y azules (clientes que se quedaron) mezcladas. El SVM busca la mejor superficie para separarlas, maximizando el espacio libre entre la frontera y las bolitas más cercanas a ella. Esa distancia se llama margen, y maximizarla es lo que hace que el modelo generalice bien a datos nuevos. Las bolitas justo en el borde son los vectores de soporte: los casos más difíciles de clasificar, y son los que definen la frontera.
El dataset contiene información de 10.000 clientes de un banco: perfil demográfico, situación financiera y comportamiento. La variable que queremos predecir es Exited: 0 si el cliente se quedó, 1 si abandonó el banco.
Lo primero que haces cuando recibes un dataset es entender qué tienes delante y si está en condiciones de usarse. No puedes construir un modelo sobre datos corruptos — basura entra, basura sale.
Shape: (10000, 18) Filas: 10,000 | Columnas: 18 RangeIndex: 10000 entries, 0 to 9999 Data columns (total 18 columns): CreditScore 10000 non-null int64 Geography 10000 non-null object Gender 10000 non-null object Age 10000 non-null int64 Balance 10000 non-null float64 NumOfProducts 10000 non-null int64 Exited 10000 non-null int64 ...
Todas las columnas tienen exactamente 10.000 valores no nulos: no hay ningún dato faltante. Los tipos son coherentes — números en int64 o float64, textos en object.
No hay valores nulos en el dataset. No hay filas duplicadas en el dataset.
El SVM calcula distancias entre puntos en el espacio de features. Si hay valores nulos ese cálculo falla o produce resultados incorrectos. Con filas duplicadas el modelo sobrepondera ciertas muestras, lo que sesga la frontera de decisión hacia esos casos repetidos.
Exited 0 7962 ← No churn (79.62%) 1 2038 ← Churn (20.38%)
Por cada cliente que se va, hay casi cuatro que se quedan. Esto no es un detalle menor — es uno de los problemas más comunes en clasificación binaria y condiciona todas las decisiones técnicas que vienen después.
Imagina que entrenas un modelo y te dice que predice con 80% de accuracy. Suena bien. Pero si ese modelo simplemente dice «no churn» para absolutamente todos los clientes, también acertaría el 80% de las veces —porque el 80% realmente no se va. El modelo habría aprendido a hacer el vago. No habría aprendido nada útil sobre los clientes que sí se van, que son exactamente los que te interesa detectar.
Por esta razón nunca usamos accuracy como métrica principal en este proyecto. Usamos ROC-AUC y Average Precision para seleccionar y evaluar el modelo, y prestamos atención especial al recall de la clase churn porque es la métrica más relevante desde negocio.
El EDA visual responde tres preguntas fundamentales antes de modelar: ¿qué variables se comportan diferente entre churners y no churners? ¿Hay relaciones problemáticas entre features? ¿Hay outliers relevantes? Las respuestas determinan qué features entran al modelo, cuáles se transforman y cómo.
Histogramas de variables numéricas por clase. Para cada variable numérica trazamos dos distribuciones superpuestas: una para los clientes que se fueron (rojo) y otra para los que se quedaron (azul). Si las distribuciones se solapan completamente, la variable no discrimina entre ambas clases. Si se separan, tiene potencial predictivo.
| Variable | Observación | Señal predictiva |
|---|---|---|
Age | Los churners tienden a ser mayores — pico alrededor de 45-50 años vs 30-35 en no churners. | Alta |
NumOfProducts | Los clientes con 3 o 4 productos tienen churn casi total. Con 1 o 2, mucho menor. | Alta |
Balance | Pico muy pronunciado en exactamente 0, con una distribución bimodal llamativa. | Media |
CreditScore | Distribuciones casi idénticas entre las dos clases. | Baja |
EstimatedSalary | Distribución prácticamente uniforme, sin separación entre clases. | Baja |
Tenure | Distribución similar entre clases, sin separación clara. | Baja |
Tasas de churn por variable categórica. En vez de contar clientes por categoría — que sería engañoso con el desbalance 80/20 — calculamos la proporción de Exited=1 dentro de cada categoría. Así comparamos en la misma escala sin que el volumen absoluto distorsione la lectura.
| Variable | Observación | Señal predictiva |
|---|---|---|
Geography | Alemania tiene una tasa de churn que duplica la de España y Francia. | Alta |
Gender | Las mujeres abandonan el banco en mayor proporción que los hombres. | Media |
IsActiveMember | Los clientes no activos tienen una tasa de churn notablemente superior. | Media |
HasCrCard | La tasa de churn es prácticamente idéntica entre quienes tienen tarjeta y quienes no. | Nula → descartada |
Heatmap de correlación. Mide la correlación lineal entre pares de variables numéricas. Un valor cercano a 1 o -1 indica que dos variables se mueven juntas, lo que puede crear multicolinealidad y distorsionar el modelo. El resultado muestra correlaciones bajas entre todas las features numéricas — cada una aporta información independiente. Las correlaciones más altas con Exited corresponden a Age y NumOfProducts, confirmando lo que ya indicaban los histogramas.
Boxplots. Complementan los histogramas mostrando mediana, rango intercuartílico y outliers de forma compacta por clase. Confirman la separación en Age, el comportamiento no lineal de NumOfProducts, y permiten cuantificar los outliers que se abordan en la siguiente sección.
No todas las columnas del dataset son útiles para el modelo. Meter features irrelevantes no solo no ayuda — añade ruido, aumenta el coste computacional y puede perjudicar la capacidad de generalización.
Descartadas:
RowNumber, CustomerId y Surname son identificadores sin ninguna relación causal con el churn. Card Type y HasCrCard mostraron en el EDA tasas de churn prácticamente idénticas entre todas sus categorías — no discriminan.
Features finales que entran al modelo:
El análisis IQR detectó 359 outliers en Age (3.6%), 60 en NumOfProducts (0.6%) y 15 en CreditScore (0.1%). Se mantienen: el StandardScaler que aplicamos después amortigua su impacto en las distancias que calcula el SVM, y en un contexto de laboratorio no justifican eliminar datos reales.
El feature engineering consiste en crear variables nuevas a partir de las existentes cuando detectas un patrón que el modelo podría no capturar bien por sí solo. No es inventar datos — es reformular la información disponible de una forma más útil para el algoritmo.
En el histograma de Balance vimos algo llamativo: una cantidad muy elevada de clientes con saldo exactamente igual a 0. No es el extremo inferior de una distribución continua — es un pico discreto en el cero exacto, que indica que «no tener saldo» es una categoría de cliente diferenciada con comportamiento propio respecto al churn.
Si metes Balance como variable numérica continua, el modelo ve 0 como «un valor bajo de saldo». Pero la diferencia real no es entre saldo bajo y saldo alto — es entre «tiene saldo» y «no tiene saldo en absoluto». Esa distinción binaria tiene sentido de negocio: un cliente sin saldo en cuenta tiene un perfil de riesgo diferente. La variable has_balance captura exactamente eso con un 0 o un 1, y Balance original se mantiene también porque aporta información complementaria sobre cuánto saldo tiene quien sí tiene.
Los algoritmos de ML, incluido el SVM, trabajan con números. Las variables categóricas necesitan convertirse a representaciones numéricas antes de entrar al modelo. Pero no todas las conversiones son iguales — elegir la incorrecta puede introducir información falsa.
Label encoding para Gender. Solo tiene dos valores: Male y Female. La convertimos directamente a 0 y 1. Funciona porque es binaria — no hay un orden que el modelo pueda malinterpretar.
Si codificas Spain=0, France=1, Germany=2, el modelo interpreta que Germany > France > Spain matemáticamente, como si hubiera una jerarquía entre países. Eso es información falsa que puede distorsionar la frontera de decisión. Geography no tiene orden — son categorías equivalentes.
One-Hot Encoding para Geography con drop_first=True: creamos una columna binaria por país, pero eliminamos una de las tres columnas resultantes para evitar multicolinealidad perfecta. Si Germany=0 y Spain=0, el modelo infiere France sin necesidad de una columna explícita.
Antes de entrenar el modelo, dividimos el dataset en dos partes que nunca se mezclan: train (80% → 8.000 filas), con el que el modelo aprende, y test (20% → 2.000 filas), reservado como simulacro de datos futuros para evaluar si lo aprendido generaliza.
El train son los apuntes y ejercicios con los que estudias. El test es el examen final — preguntas que nunca has visto antes. Si el modelo pudiera ver el test durante el entrenamiento, sería como estudiar con las respuestas del examen en la mano: los resultados serían artificialmente buenos y no reflejarían lo que el modelo realmente sabe hacer con datos nuevos.
El parámetro stratify=y es crítico con desbalance. Sin él, la división aleatoria podría generar un test donde, por azar, haya muy pocos churners. Con stratify, la proporción 80/20 de churn se replica exactamente en train y en test.
Train: (8000, 14) | Test: (2000, 14)
Features: ['CreditScore', 'Gender', 'Age', 'Tenure', 'Balance',
'NumOfProducts', 'IsActiveMember', 'EstimatedSalary',
'Satisfaction Score', 'Point Earned', 'has_balance',
'Geography_Germany', 'Geography_Spain']
El SVM calcula distancias entre puntos en el espacio de features para encontrar la frontera óptima. Si EstimatedSalary va de 0 a 200.000 y NumOfProducts va de 1 a 4, la distancia estará completamente dominada por el salario aunque NumOfProducts sea mucho más informativo. Sin scaling el modelo no puede comparar features en igualdad de condiciones. El StandardScaler lleva todo a media 0 y desviación típica 1, sin distorsionar la distribución relativa de los datos.
Si haces fit_transform sobre todo el dataset antes del split, estás filtrando información del futuro al modelo. Las métricas resultantes serían optimistas y falsas. El orden correcto es siempre: split primero, luego fit solo en train.
PCA transforma las features originales en combinaciones lineales ordenadas por la cantidad de varianza que explican. El objetivo es reducir dimensiones manteniendo la mayor información posible.
90% de varianza explicada con 12 componentes 95% de varianza explicada con 12 componentes
Para conservar el 90% de la varianza necesitamos 12 de 14 componentes — una reducción real de apenas 2 features. El coste de esa reducción es perder toda interpretabilidad: las componentes principales son combinaciones abstractas de todas las features originales, y ya no podemos decir qué variables importan más. Para ganar 2 dimensiones, no merece la pena. Decisión: entrenar directamente sobre las 14 features escaladas.
Antes de buscar los mejores hiperparámetros, entrenamos un modelo con los valores por defecto. Esto da un punto de referencia claro: si después de todo el trabajo de tuning solo mejoramos 0.01 puntos de AUC, el esfuerzo apenas aportó. Si mejoramos 0.10 puntos, el tuning fue esencial.
Usamos class_weight='balanced' desde el inicio para compensar el desbalance 80/20 — sin esto el modelo ignoraría casi completamente la clase churn. Y probability=True para obtener scores continuos entre 0 y 1, necesarios para la curva ROC y para ajustar el threshold posteriormente.
precision recall f1-score support
No churn 0.93 0.79 0.86 1592
Churn 0.49 0.78 0.60 408
accuracy 0.79 2000
macro avg 0.71 0.78 0.73 2000
weighted avg 0.84 0.79 0.80 2000
ROC-AUC: 0.8548
Un AUC de 0.8548 ya desde el baseline es un punto de partida sólido. El modelo detecta el 78% de los churners reales (recall), aunque genera bastantes falsas alarmas — de cada 2 clientes que marca como churn, solo 1 realmente lo es (precision 0.49). Ese es el equilibrio típico en problemas con desbalance: para detectar más churners reales hay que asumir más falsas alarmas, y el threshold es el dial que controla ese equilibrio.
Dado lo que mostró el EDA, las variables que más contribuyen a este resultado son probablemente Age, NumOfProducts, IsActiveMember y Geography. El SVM no da importancias de feature directamente como un árbol de decisión, pero la señal que detectamos visualmente en el EDA es la que el modelo está aprendiendo a explotar.
Los hiperparámetros son parámetros que configuramos antes de entrenar y que el modelo no puede aprender por sí solo. Para el SVM con kernel RBF, los dos críticos son C y gamma.
C es el control de rigidez. C bajo permite que la frontera ignore puntos mal clasificados para quedar suave y amplia — más tolerante pero puede subajustar. C alto obliga a clasificar casi todos los puntos correctamente aunque la frontera quede retorcida — puede sobreajustar. Gamma controla el radio de influencia de cada punto de soporte. Gamma bajo extiende esa influencia lejos, generando fronteras suaves. Gamma alto la concentra localmente, generando fronteras más irregulares que pueden memorizar el ruido.
Fitting 5 folds for each of 20 candidates, totalling 100 fits
Mejores parámetros: {'C': 100, 'gamma': 0.01}
Mejor ROC-AUC (CV): 0.8442
El ganador es C=100, gamma=0.01. C alto indica que el modelo necesita ajustarse con bastante precisión a los datos de entrenamiento para capturar la señal — tiene sentido en un problema donde los patrones no son triviales. Gamma bajo genera una frontera global y suave, lo que ayuda a generalizar a datos nuevos. La combinación es coherente: estricto en los errores de entrenamiento, pero con influencia amplia para no sobreajustar localmente.
Con desbalance 80/20, un modelo que predice siempre «no churn» tendría 80% de accuracy en cada fold — parecería el mejor modelo sin serlo. ROC-AUC mide la capacidad discriminativa global del modelo independientemente del threshold y del desbalance, por lo que es la métrica correcta para guiar la búsqueda de hiperparámetros.
Aplicamos el mejor modelo al test set — 2.000 clientes que nunca ha visto — y medimos cuánto acierta y, sobre todo, de qué tipo son sus errores.
precision recall f1-score support
No churn 0.93 0.79 0.85 1592
Churn 0.48 0.78 0.60 408
accuracy 0.79 2000
macro avg 0.71 0.78 0.73 2000
weighted avg 0.84 0.79 0.80 2000
ROC-AUC (test): 0.8622
El modelo detecta 319 de los 408 churners reales del test — un recall del 78%. Los 89 que se escapa son clientes que se van sin que el sistema los haya identificado. Por otro lado, de los 653 clientes que el modelo marca como churn, 334 en realidad no se iban a ir — esas son las falsas alarmas que generan campañas de retención innecesarias.
Un cliente que sí se va pero el modelo predice que se queda es una oportunidad perdida — no actúas sobre él y lo pierdes. Los falsos positivos generan coste en campañas de retención innecesarias, pero ese coste suele ser menor que perder un cliente. Por eso en problemas de churn el recall es la métrica que más atenemos al criterio de negocio.
Además de la curva ROC, evaluamos el modelo con la curva Precision-Recall y el Average Precision. Con desbalance importante, la curva ROC puede resultar engañosamente optimista porque el gran volumen de verdaderos negativos (los 7.962 clientes que no hacen churn) infla artificialmente el denominador del FPR y hace que la curva parezca mejor de lo que es para la clase que realmente nos importa.
Average Precision: 0.6974
El eje X es el recall — de todos los churners reales, qué porcentaje detecto. El eje Y es la precision — de los que clasifico como churn, qué porcentaje realmente lo son. La línea roja punteada es el baseline: un clasificador aleatorio que acertaría el 20% de las veces (la proporción de churners). Cuanto más arriba y a la derecha esté nuestra curva respecto al baseline, mejor discrimina el modelo sobre la clase churn. El Average Precision de 0.6974 resume el área bajo esa curva — representa una mejora sustancial sobre el azar.
La curva ROC visualiza el comportamiento del modelo para todos los posibles umbrales de decisión simultáneamente. Por defecto, el modelo clasifica como churn cuando la probabilidad estimada supera 0.5. Pero ese 0.5 es arbitrario y no tiene en cuenta ni el desbalance ni el coste relativo de cada tipo de error.
Imagina un dial entre 0 y 1. En 0, clasificas todo como churn: detectas todos los churners reales (TPR=1) pero también generas infinitas falsas alarmas (FPR=1). En 1, al revés — cero falsas alarmas pero tampoco detectas nada. La curva ROC traza el camino entre esos dos extremos para cada valor posible del dial. El AUC mide cuán arriba y a la izquierda está esa curva: 1.0 sería discriminación perfecta, 0.5 sería azar puro.
Threshold óptimo (Youden): 0.2522 TPR en ese punto: 0.7475 FPR en ese punto: 0.1715
precision recall f1-score support
No churn 0.93 0.83 0.88 1592
Churn 0.53 0.75 0.62 408
accuracy 0.81 2000
macro avg 0.73 0.79 0.75 2000
weighted avg 0.85 0.81 0.82 2000
Bajar el threshold de 0.5 a 0.2522 tiene un efecto claro en el conjunto: la accuracy global sube de 0.79 a 0.81, la precision de churn sube de 0.48 a 0.53 y el F1 de churn mejora de 0.60 a 0.62. El threshold ajustado ofrece un mejor equilibrio global aunque el recall de churn baje ligeramente de 0.78 a 0.75 — es el trade-off inherente al ajuste.
El modelo SVM con kernel RBF, C=100 y gamma=0.01, entrenado sobre las 14 features de comportamiento financiero y perfil demográfico de los clientes, alcanza un AUC de 0.8622 sobre el conjunto de test. Detecta correctamente el 75% de los clientes en riesgo de abandono cuando se usa el threshold óptimo de 0.2522.
Las features que más contribuyen a ese resultado son las que el EDA ya señalaba como relevantes: la edad del cliente, el número de productos contratados, si es miembro activo y el país de procedencia — Alemania en particular concentra una tasa de churn significativamente más alta.
Un AUC de 0.8622 es un resultado sólido para un problema de churn bancario con desbalance 80/20, trabajando únicamente con features de comportamiento observable — sin señales artificiales. El pipeline completo es robusto: split estratificado, scaling correcto, cross-validation estratificada en el GridSearch, y evaluación con métricas adecuadas al desbalance.
La precision de churn (0.48-0.53) implica que por cada churner real detectado el modelo genera casi una falsa alarma. Posibles vías de mejora: técnicas de resampling como SMOTE para el desbalance, explorar otros kernels o algoritmos más interpretables como gradient boosting, profundizar en el feature engineering especialmente en interacciones entre Age y NumOfProducts, o ajustar el threshold en función del coste real de cada tipo de error en el negocio.
Carga y verificación de calidad → análisis del desbalance 80/20 → EDA visual (histogramas 2×4, tasas de churn por categórica, heatmap de correlación, boxplots) → selección de features (5 columnas descartadas) → feature engineering (has_balance) → encoding (label para Gender, OHE con drop_first para Geography) → train/test split estratificado 80/20 → StandardScaler fit solo en train → PCA evaluado y descartado → baseline SVM con AUC 0.8548 → GridSearchCV con 20 combinaciones y 5 folds, ganador C=100, gamma=0.01 → evaluación con classification report y confusion matrix → Average Precision 0.6974 y curva PR → curva ROC con AUC 0.8622 y ajuste de threshold a 0.2522.