Skip to content

Datos y features

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

Datos y features

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

The dataset is generated, not downloaded

There is no third-party dataset behind this project. generate_data.py builds 2.000 synthetic customers with np.random.seed(42) and writes churn_data.csv.

That is a deliberate choice: a public churn dataset drags along a licence and says nothing about whether the person can model a problem. Generating it means the relationships between variables are known in advance, so the model can be checked against what was actually planted in the data.

Columns in churn_data.csv

Column Type Range / values
CustomerID int 1 … 2000
Age int 22 – 67
SubscriptionType categorical Basic (40%) · Standard (35%) · Premium (25%)
MonthlyCharges float Basic 20–60 · Standard 55–100 · Premium 95–150
TotalUsageHours float 30 – 600
SupportTickets int Poisson(2.2), capped at 12
ContractDuration_Months int 1 (45%) · 12 (32%) · 24 (23%)
TenureMonths int 1 – 59
NumProducts int 1 (40%) · 2 (30%) · 3 (20%) · 4 (10%)
Churn binary 0 / 1

MonthlyCharges is not independent of SubscriptionType: each tier draws from its own price band. That is what makes the dataset worth modelling — the variables are correlated, as they would be in a real book of customers.

How churn is planted

Each customer gets a churn probability, and the label is drawn against it:

churn_prob = (
    0.10
    + 0.22 * (support_tickets >= 5)
    + 0.18 * (monthly_charges > 100)
    + 0.15 * (contract_months == 1)
    - 0.12 * (subscription == 'Premium')
    - 0.10 * (tenure_months > 24)
    - 0.08 * (num_products >= 3)
    + 0.10 * (total_usage < 80)
    - 0.06 * (contract_months == 24)
).clip(0.02, 0.92)

churn = (np.random.uniform(0, 1, N) < churn_prob).astype(int)

Read as a business statement: support tickets are the strongest driver (+0.22), followed by a high monthly bill and a month-to-month contract. Tenure, more products and the Premium tier hold the customer.

Because the label is drawn against a probability and not assigned by a threshold, the data has genuine noise: no model can reach 100%, and one that did would be overfitting.

The 9 features that reach the model

SubscriptionType is one-hot encoded with pd.get_dummies(..., drop_first=False), and the feature list picks Premium and Standard — leaving Basic as the reference level:

FEATURES = [
    'Age', 'MonthlyCharges', 'TotalUsageHours', 'SupportTickets',
    'ContractDuration_Months', 'TenureMonths', 'NumProducts',
    'SubscriptionType_Premium', 'SubscriptionType_Standard',
]

CustomerID is not a feature — it is an identifier, and feeding it to the model would be a textbook leak of nothing but row order.

Preprocessing

Only one transformation, and it happens after the split:

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

There is no missing-value handling because the generator does not produce nulls, and no label encoding of ordinal variables because the only categorical column is resolved with one-hot.


🇪🇸 Español

El dataset se genera, no se descarga

Detrás de este proyecto no hay ningún dataset de terceros. generate_data.py construye 2.000 clientes sintéticos con np.random.seed(42) y escribe churn_data.csv.

Es una decisión deliberada: un dataset público de cancelaciones arrastra una licencia y no dice nada sobre si quien lo usa sabe modelar un problema. Generarlo significa que las relaciones entre variables se conocen de antemano, así que el modelo se puede contrastar contra lo que de verdad se plantó en los datos.

Columnas de churn_data.csv

Columna Tipo Rango / valores
CustomerID entero 1 … 2000
Age entero 22 – 67
SubscriptionType categórica Basic (40%) · Standard (35%) · Premium (25%)
MonthlyCharges decimal Basic 20–60 · Standard 55–100 · Premium 95–150
TotalUsageHours decimal 30 – 600
SupportTickets entero Poisson(2,2), tope en 12
ContractDuration_Months entero 1 (45%) · 12 (32%) · 24 (23%)
TenureMonths entero 1 – 59
NumProducts entero 1 (40%) · 2 (30%) · 3 (20%) · 4 (10%)
Churn binaria 0 / 1

MonthlyCharges no es independiente de SubscriptionType: cada nivel sortea dentro de su propia banda de precio. Eso es lo que hace que el dataset merezca un modelo — las variables están correlacionadas, como lo estarían en una cartera real.

Cómo se planta la cancelación

Cada cliente recibe una probabilidad de cancelar y la etiqueta se sortea contra ella (ver el bloque de código de arriba).

Leído como frase de negocio: las incidencias de soporte son la palanca más fuerte (+0,22), seguidas de una factura mensual alta y de un contrato mes a mes. La antigüedad, tener más productos y el nivel Premium retienen al cliente.

Como la etiqueta se sortea contra una probabilidad y no se asigna por umbral, los datos tienen ruido de verdad: ningún modelo puede llegar al 100%, y uno que llegara estaría sobreajustando.

Las 9 variables que llegan al modelo

SubscriptionType se convierte en variables ficticias con pd.get_dummies(..., drop_first=False), y la lista de variables se queda con Premium y Standard, dejando Basic como nivel de referencia.

CustomerID no es una variable: es un identificador, y dárselo al modelo sería filtrarle nada más que el orden de las filas.

Preprocesamiento

Una sola transformación, y ocurre después de partir los datos: la estandarización, ajustada solo con el tramo de entrenamiento.

No hay tratamiento de valores ausentes porque el generador no produce nulos, ni codificación de ordinales porque la única columna categórica se resuelve con variables ficticias.