Predicción de Churn y Segmentación de Clientes

¶

EDA · Machine Learning · Clustering · Recomendaciones de negocio

import pandas as pd
import numpy as np
import math
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.cluster.hierarchy import linkage, dendrogram
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier, GradientBoostingRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score, accuracy_score, roc_auc_score, precision_score, recall_score
from sklearn.cluster import KMeans

Descargar los datos¶

df = pd.read_csv('gym_churn_us.csv')
df.columns = (
    df.columns
      .str.strip()
      .str.lower()
      .str.strip('_')
)
print(df.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000 entries, 0 to 3999
Data columns (total 14 columns):
 #   Column                             Non-Null Count  Dtype  
---  ------                             --------------  -----  
 0   gender                             4000 non-null   int64  
 1   near_location                      4000 non-null   int64  
 2   partner                            4000 non-null   int64  
 3   promo_friends                      4000 non-null   int64  
 4   phone                              4000 non-null   int64  
 5   contract_period                    4000 non-null   int64  
 6   group_visits                       4000 non-null   int64  
 7   age                                4000 non-null   int64  
 8   avg_additional_charges_total       4000 non-null   float64
 9   month_to_end_contract              4000 non-null   float64
 10  lifetime                           4000 non-null   int64  
 11  avg_class_frequency_total          4000 non-null   float64
 12  avg_class_frequency_current_month  4000 non-null   float64
 13  churn                              4000 non-null   int64  
dtypes: float64(4), int64(10)
memory usage: 437.6 KB
None

Análisis exploratorio de datos (EDA)

## ¿El dataset contiene alguna característica ausente? 

print(df.describe())
            gender  near_location      partner  promo_friends        phone  \
count  4000.000000    4000.000000  4000.000000    4000.000000  4000.000000   
mean      0.510250       0.845250     0.486750       0.308500     0.903500   
std       0.499957       0.361711     0.499887       0.461932     0.295313   
min       0.000000       0.000000     0.000000       0.000000     0.000000   
25%       0.000000       1.000000     0.000000       0.000000     1.000000   
50%       1.000000       1.000000     0.000000       0.000000     1.000000   
75%       1.000000       1.000000     1.000000       1.000000     1.000000   
max       1.000000       1.000000     1.000000       1.000000     1.000000   

       contract_period  group_visits          age  \
count      4000.000000   4000.000000  4000.000000   
mean          4.681250      0.412250    29.184250   
std           4.549706      0.492301     3.258367   
min           1.000000      0.000000    18.000000   
25%           1.000000      0.000000    27.000000   
50%           1.000000      0.000000    29.000000   
75%           6.000000      1.000000    31.000000   
max          12.000000      1.000000    41.000000   

       avg_additional_charges_total  month_to_end_contract     lifetime  \
count                   4000.000000            4000.000000  4000.000000   
mean                     146.943728               4.322750     3.724750   
std                       96.355602               4.191297     3.749267   
min                        0.148205               1.000000     0.000000   
25%                       68.868830               1.000000     1.000000   
50%                      136.220159               1.000000     3.000000   
75%                      210.949625               6.000000     5.000000   
max                      552.590740              12.000000    31.000000   

       avg_class_frequency_total  avg_class_frequency_current_month  \
count                4000.000000                        4000.000000   
mean                    1.879020                           1.767052   
std                     0.972245                           1.052906   
min                     0.000000                           0.000000   
25%                     1.180875                           0.963003   
50%                     1.832768                           1.719574   
75%                     2.536078                           2.510336   
max                     6.023668                           6.146783   

             churn  
count  4000.000000  
mean      0.265250  
std       0.441521  
min       0.000000  
25%       0.000000  
50%       0.000000  
75%       1.000000  
max       1.000000  
## Observa los valores medios de las características en dos grupos: para las personas que se fueron y para las que se quedaron.

groups = df.groupby('churn')
## Traza histogramas de barras y distribuciones de características para aquellas personas que se fueron y para las que se quedaron.

features_to_plot = [
    'age',
    'avg_class_frequency_total',
    'avg_class_frequency_current_month',
    'lifetime',
    'avg_additional_charges_total',
    'contract_period',
    'month_to_end_contract',
    'partner',
    'promo_friends',
    'group_visits',
    'near_location'
]

n_cols = 3
n_rows = math.ceil(len(features_to_plot) / n_cols)

fig, axes = plt.subplots(n_rows, n_cols, figsize=(15, 4 * n_rows))
axes = axes.flatten()

for i, col in enumerate(features_to_plot):
    
    ax = axes[i]
    
    for churn_value, group in groups:
        label = 'Active' if churn_value == 0 else 'Churned'
        ax.hist(group[col], bins=30, alpha=0.6, label=label)
    
    ax.set_title(col)
    ax.set_xlabel(col)
    ax.set_ylabel('Frequency')
    ax.legend()

for j in range(i + 1, len(axes)):
    fig.delaxes(axes[j])

plt.tight_layout()
plt.show()
No description has been provided for this image
# Matriz de correlación.

num_df = df.select_dtypes(include='number')

corr_matrix = num_df.corr()

plt.figure(figsize=(8, 8))
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', square=True)
plt.title('Correlation Matrix')
plt.show()
No description has been provided for this image

La matriz de correlación muestra que la cancelación de clientes está principalmente asociada con el nivel de compromiso y estabilidad del usuario. En particular, lifetime presenta la relación más fuerte con el churn (-0.44), seguida de la frecuencia de asistencia en el mes actual avg_class_frequency_current_month (-0.41), la edad (-0.40) y las condiciones contractuales, tanto contract_period (-0.39) como month_to_end_contract (-0.38). Estas relaciones indican que los clientes con menor tiempo de permanencia, menor participación reciente, contratos más cortos y de menor vigencia restante tienen una probabilidad significativamente mayor de cancelar su membresía.

Adicionalmente, variables como avg_class_frequency_total (-0.25), avg_additional_charges_total (-0.20), group_visits (-0.18), partner (-0.16) y promo_friends (-0.16) refuerzan la misma tendencia: a menor nivel de interacción, inversión y vínculo social, mayor propensión al churn. En conjunto, estos resultados confirman que el abandono ocurre predominantemente en las primeras etapas de la relación con el gimnasio y está impulsado por bajos niveles de engagement, proporcionando una base sólida y cuantificable para la construcción del modelo predictivo y para el diseño de estrategias de retención orientadas a fortalecer el compromiso temprano del cliente.

Construir un modelo para predecir la cancelación de usuarios

# Dividir los datos en conjuntos de entrenamiento y validación

X = df.drop(columns=['churn'])
y = df['churn']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Escalado de datos
scaler = StandardScaler()

X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Crear los modelos
log_model = LogisticRegression(max_iter=2000, random_state=42)
rf_model = RandomForestClassifier(random_state=42)

# Entrenar los modelos
log_model.fit(X_train_scaled, y_train)
rf_model.fit(X_train, y_train)

# Regresión Logística
log_preds = log_model.predict(X_test_scaled)

log_accuracy = accuracy_score(y_test, log_preds)
log_precision = precision_score(y_test, log_preds)
log_recall = recall_score(y_test, log_preds)

# Random Forest
rf_preds = rf_model.predict(X_test)

rf_accuracy = accuracy_score(y_test, rf_preds)
rf_precision = precision_score(y_test, rf_preds)
rf_recall = recall_score(y_test, rf_preds)

# Obtener probabilidades de predicción
log_probs = log_model.predict_proba(X_test_scaled)[:, 1]  
rf_probs = rf_model.predict_proba(X_test)[:, 1]          

# Calcular AUC-ROC
log_auc = roc_auc_score(y_test, log_probs)
rf_auc = roc_auc_score(y_test, rf_probs)

# Actualizar la presentación de resultados
print("Logistic Regression:")
print(" Accuracy :", round(log_accuracy, 3))
print(" Precision:", round(log_precision, 3))
print(" Recall   :", round(log_recall, 3))
print(" AUC-ROC  :", round(log_auc, 3))
print()

print("Random Forest:")
print(" Accuracy :", round(rf_accuracy, 3))
print(" Precision:", round(rf_precision, 3))
print(" Recall   :", round(rf_recall, 3))
print(" AUC-ROC  :", round(rf_auc, 3))

models = ['Logistic Regression', 'Random Forest']

accuracy = [log_accuracy, rf_accuracy]
precision = [log_precision, rf_precision]
recall = [log_recall, rf_recall]
auc = [log_auc, rf_auc]

x = range(len(models))

plt.figure()

plt.plot(x, accuracy, marker='o', label='Accuracy')
plt.plot(x, precision, marker='o', label='Precision')
plt.plot(x, recall, marker='o', label='Recall')
plt.plot(x, auc, marker='o', label='AUC-ROC')

plt.xticks(list(x), models)
plt.xlabel('Model')
plt.ylabel('Score')
plt.title('Model Performance Comparison')
plt.legend()

plt.show()
Logistic Regression:
 Accuracy : 0.925
 Precision: 0.88
 Recall   : 0.83
 AUC-ROC  : 0.977

Random Forest:
 Accuracy : 0.925
 Precision: 0.884
 Recall   : 0.825
 AUC-ROC  : 0.968
No description has been provided for this image

Ambos modelos evaluados presentan un desempeño alto y consistente en la predicción de cancelación de clientes. La Regresión Logística alcanzó una exactitud de 0.925, una precisión de 0.88, un recall de 0.83 y un AUC-ROC de 0.977, lo que indica una excelente capacidad de discriminación entre clientes que permanecen y clientes que cancelan. Por su parte, el Random Forest obtuvo resultados ligeramente superiores en métricas de clasificación directa, con una exactitud de 0.928, una precisión de 0.885, un recall de 0.835 y un AUC-ROC de 0.968.

Aunque el Random Forest ofrece una mejora pequeña en exactitud, precisión y especialmente su mejor desempeño en recall, la métrica más crítica para estrategias de retención, la Regresión Logística muestra un AUC-ROC superior, lo que refleja una capacidad de separación global más robusta. En conjunto, ambos modelos son altamente efectivos; sin embargo, el Random Forest se recomienda como modelo principal para la detección operativa de churn debido a su mejor equilibrio en las métricas clave de decisión, mientras que la Regresión Logística resulta especialmente valiosa para interpretación y análisis estratégico del comportamiento del cliente.

Crear clústeres de usuarios/as

# Preparar datos para clustering

X_clustering = df.drop(columns=['churn'])
scaler_clustering = StandardScaler()
X_clustering_scaled = scaler_clustering.fit_transform(X_clustering)
# Aplicar la función linkage
linkage_matrix = linkage(X_clustering_scaled, method='ward')

# Dendrograma completo
plt.figure(figsize=(14, 6))
dendrogram(linkage_matrix, no_labels=True)
plt.title('Dendrograma (Clustering jerárquico - Ward)')
plt.xlabel('Observaciones')
plt.ylabel('Distancia')
plt.show()
No description has been provided for this image
# Entrenamiento del modelo de clustering con el algortimo K-means.

kmeans = KMeans(n_clusters=5, random_state=42)
cluster_labels = kmeans.fit_predict(X_clustering_scaled)
# valores medios de característica para los clústeres.

# Agregar las etiquetas de clúster al DataFrame
df_with_clusters = df.copy()
df_with_clusters['cluster'] = cluster_labels

# Churn por clúster
churn_by_cluster = df_with_clusters.groupby('cluster')['churn'].mean().sort_index()

plt.figure(figsize=(8, 4))
plt.bar(churn_by_cluster.index, churn_by_cluster.values)

plt.title('Churn rate por cluster')
plt.xlabel('Cluster')
plt.ylabel('Churn rate (mean)')
plt.ylim(0, 1)

plt.show()
No description has been provided for this image
cols = [
    'contract_period',
    'month_to_end_contract',
    'lifetime',
    'avg_class_frequency_total',
    'avg_class_frequency_current_month',
    'group_visits',
    'promo_friends',
    'partner',
    'churn'
]

cluster_summary = (
    df_with_clusters
    .groupby('cluster')[cols]
    .mean()
)

# Para que el churn sea verificable como porcentaje con 1 decimal:
cluster_summary_display = cluster_summary.copy()
cluster_summary_display['churn_%'] = (cluster_summary_display['churn'] * 100).round(1)
cluster_summary_display = cluster_summary_display.drop(columns=['churn']).round(2)

display(cluster_summary_display)
contract_period month_to_end_contract lifetime avg_class_frequency_total avg_class_frequency_current_month group_visits promo_friends partner churn_%
cluster
0 1.73 1.66 2.09 1.26 0.99 0.26 0.01 0.30 58.8
1 2.74 2.54 3.53 1.62 1.49 0.43 1.00 0.79 28.6
2 10.50 9.48 4.69 2.89 2.89 0.51 0.49 0.77 1.4
3 11.19 10.35 4.82 1.15 1.14 0.58 0.42 0.74 4.2
4 2.12 2.01 4.84 2.67 2.65 0.46 0.05 0.18 10.5

Cluster 2 — Cliente ideal / premium

Churn ≈ 1.4 %

Este grupo representa a los clientes más valiosos y estables del negocio. Presentan contratos largos (contract_period = 10.50 meses, month_to_end_contract = 9.48 meses), la mayor frecuencia de uso del servicio (avg_class_frequency_current_month = 2.89) y una alta integración social (group_visits = 0.51, promo_friends = 0.49).

Su nivel de churn prácticamente nulo refleja una relación sólida y madura con el gimnasio, donde el cliente percibe alto valor y mantiene hábitos de uso plenamente consolidados.

Cluster 1 — Cliente comprometido y estable

Churn ≈ 28.6 %

Aunque sus contratos son más cortos que los del Cluster 2 (contract_period = 2.74 meses), este grupo mantiene una frecuencia de uso elevada (avg_class_frequency_current_month = 1.49), así como una buena permanencia (lifetime = 3.53 meses).

Estos clientes han desarrollado una rutina relativamente estable y sostienen un vínculo operativo consistente con el servicio, lo que mitiga parcialmente el riesgo de cancelación asociado a su menor duración contractual.

Cluster 0 — Cliente de alto riesgo

Churn ≈ 58.8 %

Este es el grupo con mayor probabilidad de abandono. Se caracteriza por contratos muy cortos (contract_period = 1.73 meses), baja frecuencia de uso (avg_class_frequency_current_month = 0.99), baja integración social (group_visits = 0.26, promo_friends = 0.01) y una permanencia extremadamente reducida (lifetime = 2.09 meses).

Este perfil describe clientes poco comprometidos, con hábitos débiles y mínima vinculación social, lo que los convierte en la principal prioridad de intervención y retención.

Cluster 4 — Cliente de valor medio con riesgo latente

Churn ≈ 10.5 %

Estos clientes muestran una buena frecuencia de uso (avg_class_frequency_current_month = 2.65) y una permanencia considerable (lifetime = 4.84 meses), pero mantienen contratos relativamente cortos (contract_period = 2.12 meses) y baja integración social (promo_friends = 0.05).

Aunque su churn es inferior al de los clusters 0 y 1, su perfil indica un nivel de compromiso aún frágil, lo que sugiere oportunidades claras de fortalecimiento mediante programas de fidelización.

Cluster 3 — Cliente de alta estabilidad contractual

Churn ≈ 4.2 %

Este segmento presenta los contratos más largos del sistema (contract_period = 11.19 meses, month_to_end_contract = 10.35 meses) y una elevada permanencia (lifetime = 4.82 meses), aunque con una frecuencia de uso moderada (avg_class_frequency_current_month = 1.14).

Se trata de clientes estructuralmente estables que mantienen su vínculo principalmente a través del compromiso contractual, incluso cuando su actividad operativa no es tan alta como la del Cluster 2.

Conclusiones y recomendaciones básicas sobre el trabajo con clientes

El Cluster 0 se identifica como el segmento de mayor riesgo, con una tasa de churn cercana al 58.8 %, explicada principalmente por un compromiso contractual y operativo extremadamente bajo. Este grupo presenta el contract_period más corto del sistema (1.73 meses) y un month_to_end_contract de 1.66 meses, lo que indica clientes con vínculos contractuales débiles desde el inicio.

Además, muestran el menor nivel de engagement del conjunto, con avg_class_frequency_total = 1.26, avg_class_frequency_current_month = 0.99 y group_visits = 0.26, todos entre los valores más bajos de los clústeres. A esto se suma un perfil de clientes muy recientes y poco vinculados, con lifetime = 2.09 meses, promo_friends = 0.01 y partner = 0.30, lo que refleja una integración social y comercial mínima con el servicio.

En contraste, el Cluster 2, el más estable y valioso, registra un churn de apenas 1.4 %, sustentado por condiciones opuestas: contract_period = 10.50 meses (más de seis veces superior al del Cluster 0), month_to_end_contract = 9.48 meses y una participación significativamente mayor con avg_class_frequency_current_month = 2.89, casi tres veces la actividad observada en el segmento de mayor riesgo.

Esta comparación demuestra que la combinación de contratos largos y alto engagement temprano es determinante para la retención y el valor de vida del cliente.

A partir de estos hallazgos, se recomienda implementar estrategias específicas para el Cluster 0, enfocadas en intervenir durante los primeros meses de relación: extensiones contractuales con incentivos, programas de onboarding intensivo, sesiones de acompañamiento inicial, beneficios por referidos y actividades grupales que fortalezcan la integración social. Dado que este segmento concentra la mayor pérdida potencial de clientes, una intervención temprana y focalizada puede generar un impacto significativo en la reducción global del churn.