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()
# 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()
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
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()
# 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()
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.