Skip to content

Modelo de ML

Mindset & Code edited this page Aug 17, 2026 · 4 revisions

Modelo de ML

🇬🇧 English first · 🇪🇸 Español más abajo.

Goal

Classify subscription customers into churners and non-churners, and expose the weight of each factor so a retention team can act on the cause instead of the symptom.

Pipeline

flowchart TD
    A["churn_data.csv"] --> B["pd.get_dummies on SubscriptionType"]
    B --> C["9 features selected"]
    C --> D["train_test_split · test_size=0.25 · stratify=y"]
    D --> E["StandardScaler — fit on train, transform on test"]
    E --> F["LogisticRegression(max_iter=1000, class_weight='balanced')"]
    F --> G["predict + predict_proba"]
    G --> H["classification_report · confusion_matrix · roc_auc_score"]
    H --> I["JSON export to data/"]
Loading

The model, as it is in the code

from sklearn.linear_model import LogisticRegression

model = LogisticRegression(max_iter=1000, class_weight='balanced', random_state=42)
model.fit(X_train_s, y_train)

There is no grid search and no cross-validation: a single fit with a fixed seed, so the run is reproducible.

Split and scaling

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

scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s  = scaler.transform(X_test)

Two details that are easy to get wrong and are done right here:

  • stratify=y keeps the churn share identical in train and test. Without it, a random split can hand the test set a different class balance and the metrics stop being comparable.
  • The scaler is fitted on train only. fit_transform on train, plain transform on test. Fitting on the whole dataset would leak test statistics into training.

Metrics that are computed

Metric Where it comes from
Accuracy classification_report(...)['accuracy']
Precision, recall, F1 classification_report for class 1 (churn)
AUC-ROC roc_auc_score(y_test, y_proba) over predict_proba[:, 1]
Confusion matrix confusion_matrix(y_test, y_pred), exported as tn/fp/fn/tp

Accuracy on its own says little here: the classes are uneven, and a model that never predicts churn already scores well. Recall on the churn class and the AUC-ROC are the ones that carry information.

The live figures are the ones published in the dashboard — see Dashboard interactivo. Running python churn_analysis.py prints them to stdout and rewrites the JSON files.

Coefficients, not feature importances

feature_importance = pd.Series(model.coef_[0], index=FEATURES).sort_values()

A logistic regression has no feature_importances_. What it has is one coefficient per feature, and because the features were standardised the coefficients are comparable to each other. The sign is the part that matters: a positive coefficient pushes towards churn, a negative one holds the customer.

That is why the chart in the dashboard is a diverging bar chart around zero, and why the JSON field is called coefficient and not importance.

Reproducing it

python generate_data.py    # writes churn_data.csv
python churn_analysis.py   # trains, prints metrics, writes data/*.json

Requires pandas, numpy, scikit-learn, matplotlib and seaborn. Both scripts fix random_state=42 / np.random.seed(42), so two runs on the same machine give the same numbers.


🇪🇸 Español

Objetivo

Clasificar clientes de suscripción en cancelan y permanecen, y exponer el peso de cada factor para que un equipo de retención actúe sobre la causa y no sobre el síntoma.

Pipeline

Ver el diagrama de arriba. En texto: se convierte el tipo de suscripción en variables ficticias, se seleccionan 9 variables, se parte en 75/25 de forma estratificada, se estandariza ajustando solo con el tramo de entrenamiento, se entrena la regresión logística y se calculan las métricas.

El modelo, tal y como está en el código

from sklearn.linear_model import LogisticRegression

model = LogisticRegression(max_iter=1000, class_weight='balanced', random_state=42)
model.fit(X_train_s, y_train)

No hay búsqueda de hiperparámetros ni validación cruzada: un único ajuste con semilla fija, para que la ejecución sea reproducible.

Partición y escalado

Dos detalles que es fácil hacer mal y que aquí están bien:

  • stratify=y mantiene idéntica la proporción de cancelaciones en entrenamiento y en prueba. Sin eso, una partición aleatoria puede dejar al tramo de prueba otro balance de clases y las métricas dejan de ser comparables.
  • El escalador se ajusta solo con el entrenamiento. fit_transform en train y transform a secas en test. Ajustarlo sobre todo el conjunto filtraría al entrenamiento información del tramo de prueba.

Métricas que se calculan

Métrica De dónde sale
Acierto (accuracy) classification_report(...)['accuracy']
Precisión, recall, F1 classification_report para la clase 1 (cancela)
AUC-ROC roc_auc_score(y_test, y_proba) sobre predict_proba[:, 1]
Matriz de confusión confusion_matrix(y_test, y_pred), exportada como tn/fp/fn/tp

El acierto por sí solo dice poco aquí: las clases no están igualadas y un modelo que no prediga nunca una cancelación ya puntúa bien. El recall sobre la clase que cancela y el AUC-ROC son los que llevan información.

Las cifras vigentes son las publicadas en el cuadro de mando — ver Dashboard interactivo. Ejecutar python churn_analysis.py las imprime por pantalla y reescribe los JSON.

Coeficientes, no feature importances

Una regresión logística no tiene feature_importances_. Lo que tiene es un coeficiente por variable y, como las variables se estandarizaron antes, los coeficientes son comparables entre sí. Lo que importa es el signo: un coeficiente positivo empuja hacia la cancelación, uno negativo retiene al cliente.

Por eso el gráfico del cuadro de mando es de barras divergentes alrededor del cero, y por eso el campo del JSON se llama coefficient y no importance.

Cómo reproducirlo

python generate_data.py    # escribe churn_data.csv
python churn_analysis.py   # entrena, imprime métricas y escribe data/*.json

Necesita pandas, numpy, scikit-learn, matplotlib y seaborn. Los dos scripts fijan random_state=42 / np.random.seed(42), así que dos ejecuciones en la misma máquina dan los mismos números.