Dataset Guide¶
Before you model anything, you need to understand what you are working with. This guide walks through the dataset, every column, the formula that generates churn labels, and what that means for how you should approach this problem.
Generating the Dataset¶
Run this block once at the top of every notebook in this project. It produces a reproducible 1,000-customer dataset with realistic patterns and injected missingness.
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 1000
# --- raw features ---
tenure = rng.integers(1, 72, n) # months as customer, 1–71
age = rng.integers(22, 65, n).astype(float) # customer age
segment = rng.choice(
["Basic", "Premium", "Enterprise"], n, p=[0.55, 0.35, 0.10]
)
region = rng.choice(["North", "South", "East", "West"], n)
support_calls = rng.integers(0, 15, n).astype(float) # calls to support in last 90 days
monthly_fee = np.where(
segment == "Enterprise", rng.uniform(2000, 5000, n),
np.where(segment == "Premium", rng.uniform(800, 1500, n),
rng.uniform(200, 600, n))
)
num_products = rng.integers(1, 6, n) # active products / add-ons
has_contract = rng.choice([0, 1], n, p=[0.4, 0.6]) # signed annual contract?
# --- churn probability formula ---
churn_prob = (
0.55
- tenure * 0.008
+ support_calls * 0.04
- (segment == "Premium") * 0.12
- (segment == "Enterprise") * 0.20
- has_contract * 0.18
+ rng.normal(0, 0.05, n)
)
churn_prob = np.clip(churn_prob, 0.02, 0.95)
churn = (rng.uniform(size=n) < churn_prob).astype(int)
# --- assemble dataframe ---
df = pd.DataFrame({
"customer_id": range(10001, 10001 + n),
"age": age,
"tenure_months": tenure,
"segment": segment,
"region": region,
"support_calls": support_calls,
"monthly_fee": monthly_fee.round(2),
"num_products": num_products,
"has_contract": has_contract,
"churn": churn,
})
# --- inject realistic missingness ---
df.loc[rng.choice(df.index, 30, replace=False), "age"] = np.nan # ~3%
df.loc[rng.choice(df.index, 15, replace=False), "support_calls"] = np.nan # ~1.5%
print(df.shape) # (1000, 10)
print(df.churn.mean()) # ~0.29–0.32
Column Reference¶
| Column | Type | Description | Missingness |
|---|---|---|---|
customer_id |
int | Unique customer identifier (10001–11000) | None |
age |
float | Customer age in years (22–64) | ~3% missing |
tenure_months |
int | Months the customer has been active (1–71) | None |
segment |
str | Subscription tier: Basic, Premium, Enterprise | None |
region |
str | Geographic region: North, South, East, West | None |
support_calls |
float | Calls to customer support in the past 90 days (0–14) | ~1.5% missing |
monthly_fee |
float | Monthly subscription fee in INR (varies by segment) | None |
num_products |
int | Number of active products or add-ons (1–5) | None |
has_contract |
int | 1 if customer signed an annual contract, 0 otherwise | None |
churn |
int | Target: 1 = cancelled in the next 30 days, 0 = retained | None |
Understanding the Churn Formula¶
The churn probability for each customer is:
P(churn) = 0.55
− tenure_months × 0.008
+ support_calls × 0.040
− (Premium) × 0.120
− (Enterprise) × 0.200
− has_contract × 0.180
+ noise ~ N(0, 0.05)
Each term reflects a real-world insight:
Tenure reduces churn (−0.008 per month)
Long-tenured customers have embedded habits and sunk costs. A customer of 60 months has a base probability 48 percentage points lower than a brand-new customer. This matches the empirical finding that early months are the highest-risk period.
Support calls increase churn (+0.04 per call)
A customer who calls support repeatedly is signalling friction. Seven calls adds 28 percentage points of churn risk. In real telco datasets (IBM Telco, Orange Telecom), this is consistently one of the top three predictors.
Enterprise customers churn least (−0.20)
Enterprise contracts involve procurement cycles, executive sign-off, and integration work. Switching costs are enormous. Enterprise churn happens, but it is rare and usually a long-term organisational decision, not an impulsive cancel.
Annual contracts retain customers (−0.18)
A contract creates friction for leaving. Even when a customer is unhappy, they often wait until contract renewal rather than pay an early-exit fee.
The noise term adds realism. No dataset perfectly follows a formula. The noise ensures that not every high-tenure Enterprise customer is guaranteed to stay, and not every new Basic customer is guaranteed to leave.
Info
The intercept of 0.55 means a new customer (tenure = 0) with no support calls, no contract, on the Basic plan starts with a 55% base churn probability. That is intentionally high — new customers churn at high rates before they see value.
Class Imbalance¶
With this formula and seed, approximately 28–32% of customers churn. That gives you a ratio of roughly 7:3 (retained:churned).
print(df['churn'].value_counts())
# 0 ~700
# 1 ~300
print(f"Churn rate: {df['churn'].mean():.1%}")
# Churn rate: ~30.0%
This is a moderate imbalance — severe enough to make accuracy a misleading metric, but not so extreme that you need exotic resampling techniques like SMOTE. A classifier that always predicts "retained" achieves 70% accuracy while catching zero churners. Your job is to do better than that baseline.
Warning
Never report accuracy alone on an imbalanced dataset. A model with 70% accuracy and 0% recall on churners is worse than useless — it gives false confidence while missing every customer you need to call.
Why Synthetic Data Works Here¶
Synthetic data trained good habits:
- The signal-to-noise ratio and feature relationships are controllable and transparent
- You know the ground truth:
tenure_monthsandsupport_callsare the two dominant signals. You can validate that your EDA and feature importance results confirm this. - No PII concerns, no download, no licence issues
The patterns in this dataset mirror real-world churn datasets. Once you finish this project, apply the same workflow to the IBM Telco Customer Churn dataset on Kaggle — it has the same structure (tenure, contract type, support interactions, monthly charges) and will feel immediately familiar.
Tip
Extension: IBM Telco Churn on Kaggle has 7,043 rows and 20 features. The target column is Churn (Yes/No — convert to 1/0). The variable tenure maps to tenure_months, Contract maps to has_contract, and MonthlyCharges maps to monthly_fee. Your entire pipeline from this project transfers with minimal changes.