Skip to content

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_months and support_calls are 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.


Previous: README | Next: eda