Repository navigation
Datos y features
🇬🇧 English first · 🇪🇸 Español más abajo.
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.
| 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.
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.
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.
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.
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.
| 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.
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.
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.
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.