SkillNavoraData Science & AI Handbook
Python for Data Science · Lesson 22 of 29

Exploratory Data Analysis (EDA) Case Study: Telecom Customer Churn

Run a complete, structured EDA on 3,000 telecom customers. Frame the question, check data quality, do univariate, bivariate and segment analysis, engineer features, quantify revenue at risk and finish with findings and recommendations: the template you will reuse for every dataset and for the ML churn model.

● Python class 17✦ pandas 3 · Seaborn 0.13Intermediate⏱ 150 min
⬇ Download PDF
By the end of this lesson you can
  • Follow a repeatable EDA checklist from question to recommendation
  • Check data quality and consistency between related columns
  • Analyse univariate distributions and bivariate relationships with the target
  • Build segments and quantify business impact (revenue at risk)
  • Write findings as clear, numbered insights with actions
Datasets used

The EDA checklist

A structured EDA moves from the business question to actions.
A structured EDA moves from the business question to actions.

Business question. A telecom operator loses customers every month. Which customers churn, why, and where should retention money go first? Target: churn (Yes/No). Metric: churn rate = churned ÷ customers.

1. Load and first look

Python
import pandas as pd, numpy as np, seaborn as sns, matplotlib.pyplot as plt
pd.set_option("display.max_columns", 20); pd.set_option("display.width", 160)
tc = pd.read_csv("telecom_churn.csv")
tc["churned"] = (tc["churn"] == "Yes").astype(int)
print(tc.shape); print(tc.head(3))
print(tc.dtypes.astype(str).to_dict())
print(f"Overall churn rate: {tc.churned.mean():.1%} ({tc.churned.sum()} of {len(tc)})")
Output
(3000, 11)
  customer_id  senior_citizen  tenure_months        contract internet_service payment_method  monthly_charges  total_charges  support_calls churn  churned
0      T00001               0             71        Two year            Fiber    Net banking            81.28        5848.06              1    No        0
1      T00002               1             56  Month-to-month            Fiber     Auto-debit           115.12        6482.44              0    No        0
2      T00003               0             38        Two year            Fiber     Auto-debit            85.90        3186.34              1    No        0
{'customer_id': 'str', 'senior_citizen': 'int64', 'tenure_months': 'int64', 'contract': 'str', 'internet_service': 'str', 'payment_method': 'str', 'monthly_charges': 'float64', 'total_charges': 'float64', 'support_calls': 'int64', 'churn': 'str', 'churned': 'int64'}
Overall churn rate: 25.7% (772 of 3000)

2. Data quality

Python
print("missing:", tc.isna().sum().sum(), "| duplicate ids:", tc.customer_id.duplicated().sum())
print(tc.describe().round(1).T[["min", "50%", "max", "mean"]])
for c in ["contract", "internet_service", "payment_method"]: print(c, tc[c].value_counts().to_dict())
gap = tc.total_charges - tc.tenure_months * tc.monthly_charges        # consistency check between columns
print("total_charges − tenure × monthly: median", round(gap.median(), 1), "| 95% within ±", round(gap.abs().quantile(.95), 0))
print("tenure 0 customers:", (tc.tenure_months == 0).sum())
Output
missing: 0 | duplicate ids: 0
                  min     50%     max    mean
senior_citizen    0.0     0.0     1.0     0.2
tenure_months     0.0    36.0    72.0    36.1
monthly_charges  20.0    73.8   131.7    67.1
total_charges    19.6  2161.3  8724.4  2418.3
support_calls     0.0     1.0     6.0     1.3
churned           0.0     0.0     1.0     0.3
contract {'Month-to-month': 1690, 'One year': 739, 'Two year': 571}
internet_service {'Fiber': 1368, 'DSL': 1047, 'No internet': 585}
payment_method {'Card': 767, 'UPI': 753, 'Auto-debit': 746, 'Net banking': 734}
total_charges − tenure × monthly: median -0.8 | 95% within ± 189.0
tenure 0 customers: 45

No missing values or duplicate IDs. total_charges roughly equals tenure × monthly charges (small gaps from price changes), so the columns are consistent. A few customers have tenure 0 (new joiners), which is valid.

3. Univariate analysis

Distributions of the numeric variables. Tenure is spread across 0–72 months; monthly charges have a bump of low-price plans.
Distributions of the numeric variables. Tenure is spread across 0–72 months; monthly charges have a bump of low-price plans.

4. Bivariate: each driver vs churn

Python
def churn_by(col):
    g = tc.groupby(col, observed=True)["churned"].agg(customers="size", churn_rate="mean")
    g["churn_rate"] = (100 * g.churn_rate).round(1); g["lift"] = (g.churn_rate / (100 * tc.churned.mean())).round(2)
    return g
for col in ["contract", "internet_service", "payment_method", "senior_citizen"]: print(churn_by(col), "\n")
Output
                customers  churn_rate  lift
contract                                   
Month-to-month       1690        35.9  1.40
One year              739        16.9  0.66
Two year              571         7.0  0.27 

                  customers  churn_rate  lift
internet_service                             
DSL                    1047        21.8  0.85
Fiber                  1368        32.2  1.25
No internet             585        17.6  0.68 

                customers  churn_rate  lift
payment_method                             
Auto-debit            746        23.5  0.91
Card                  767        26.3  1.02
Net banking           734        28.5  1.11
UPI                   753        24.7  0.96 

                customers  churn_rate  lift
senior_citizen                             
0                    2490        25.3  0.98
1                     510        27.8  1.08

Lift = segment churn ÷ overall churn. Lift 1.4 means 40% more likely to churn than average.

Python
tc["tenure_band"] = pd.cut(tc.tenure_months, [0, 6, 12, 24, 48, 72], include_lowest=True, labels=["0–6", "7–12", "13–24", "25–48", "49–72"])
tc["calls_band"] = pd.cut(tc.support_calls, [-1, 0, 1, 2, 3, 10], labels=["0", "1", "2", "3", "4+"])
tc["charge_band"] = pd.qcut(tc.monthly_charges, 4, labels=["Q1 low", "Q2", "Q3", "Q4 high"])
for col in ["tenure_band", "calls_band", "charge_band"]: print(churn_by(col), "\n")
Output
             customers  churn_rate  lift
tenure_band                             
0–6                318        51.9  2.02
7–12               238        44.5  1.73
13–24              452        38.5  1.50
25–48             1011        21.6  0.84
49–72              981        11.1  0.43 

            customers  churn_rate  lift
calls_band                             
0                 822        19.1  0.74
1                1101        23.1  0.90
2                 649        30.0  1.17
3                 294        36.4  1.41
4+                134        44.0  1.71 

             customers  churn_rate  lift
charge_band                             
Q1 low             781        19.0  0.74
Q2                 719        20.2  0.78
Q3                 750        29.9  1.16
Q4 high            750        34.0  1.32
Churn rate by the four strongest drivers. The dashed line is the overall rate.
Churn rate by the four strongest drivers. The dashed line is the overall rate.

Numeric drivers

Python
print(tc.groupby("churn")[["tenure_months", "monthly_charges", "total_charges", "support_calls"]].median())
print(tc[["tenure_months", "monthly_charges", "support_calls", "senior_citizen", "churned"]].corr()["churned"].drop("churned").round(3).sort_values())
Output
       tenure_months  monthly_charges  total_charges  support_calls
churn                                                              
No              42.0           68.010       2445.905            1.0
Yes             21.0           75.405       1357.910            1.0
tenure_months     -0.322
senior_citizen     0.022
monthly_charges    0.143
support_calls      0.153
Name: churned, dtype: float64

5. Segments: interactions matter

Python
seg = tc.pivot_table(index="contract", columns="tenure_band", values="churned", aggfunc="mean", observed=True).mul(100).round(1)
print(seg)
cnt = tc.pivot_table(index="contract", columns="tenure_band", values="churned", aggfunc="size", observed=True)
print(cnt)
Output
tenure_band      0–6  7–12  13–24  25–48  49–72
contract                                       
Month-to-month  70.2  62.4   53.4   29.7   17.4
One year        36.5  30.3   26.9   13.4    4.3
Two year        13.5  17.0   10.7    6.8    1.5
tenure_band     0–6  7–12  13–24  25–48  49–72
contract                                      
Month-to-month  181   125    249    583    552
One year         85    66    119    238    231
Two year         52    47     84    190    198
Churn rate (%) by contract and tenure: new month-to-month customers are the hot spot.
Churn rate (%) by contract and tenure: new month-to-month customers are the hot spot.
Python
hot = tc[(tc.contract == "Month-to-month") & (tc.tenure_months <= 12)]
fiber_calls = tc[(tc.internet_service == "Fiber") & (tc.support_calls >= 3)]
for name, d in [("Month-to-month, tenure ≤ 12", hot), ("Fiber with 3+ support calls", fiber_calls)]:
    print(f"{name:32s} customers {len(d):4d} | churn {d.churned.mean():.1%} | lift {d.churned.mean() / tc.churned.mean():.2f}")
Output
Month-to-month, tenure ≤ 12      customers  306 | churn 67.0% | lift 2.60
Fiber with 3+ support calls      customers  192 | churn 46.9% | lift 1.82

6. Business impact: revenue at risk

Python
tc["monthly_revenue_lost"] = tc.monthly_charges * tc.churned
lost = tc.monthly_revenue_lost.sum()
print(f"Monthly revenue of churned customers: {lost:,.0f}  ({lost / tc.monthly_charges.sum():.1%} of all monthly revenue)")
imp = tc.groupby("contract").agg(churners=("churned", "sum"), revenue_lost=("monthly_revenue_lost", "sum"))
imp["share_of_loss_%"] = (100 * imp.revenue_lost / lost).round(1)
print(imp.round(0))
Output
Monthly revenue of churned customers: 56,173  (27.9% of all monthly revenue)
                churners  revenue_lost  share_of_loss_%
contract                                               
Month-to-month       607       43293.0             77.0
One year             125        9763.0             17.0
Two year              40        3117.0              6.0

Revenue numbers here are in the data's currency units per month; multiply by expected remaining lifetime to estimate lifetime value lost.

7. Findings and recommendations

Key takeaways
  1. About 1 in 4 customers churn (25.7%), taking 27.9% of monthly revenue with them.
  2. Contract is the strongest driver: month-to-month churn is about 36%, against about 7% on two-year contracts, and month-to-month customers account for 77% of the monthly revenue lost.
  3. The first year is critical: churn is above 40% in the first 12 months and falls steadily with tenure.
  4. Support calls signal risk: churn roughly doubles from 0 calls (about 19%) to 3 or more calls (about 36–44%).
  5. Fiber customers churn more (about 32% against 22% on DSL), which is consistent with price or service-quality issues.
  6. Payment method and senior status make only small differences.

Recommendations. (a) Offer month-to-month customers in their first year an incentive to move to an annual plan. (b) Trigger a proactive call after the second support ticket. (c) Investigate Fiber service quality and pricing. (d) Build a churn-score model (ML track, capstone) to rank customers each month and measure the retention campaign with an A/B test.

NoteEDA is not causal proof

These are associations. Contract and churn are related, but customers who intend to leave may simply choose month-to-month plans. Experiments (A/B tests) or careful causal methods are needed before claiming that a change will cause lower churn.

Automated profiling tools

Libraries such as ydata-profiling (ProfileReport(df)), sweetviz and D-Tale generate an EDA report in one line, and AI assistants (Lesson ai-analytics) can draft charts. Use them for a quick first scan, then do the focused, question-driven analysis above.

Summary

Key takeaways
  • EDA follows a checklist: question → quality → univariate → bivariate → segments → impact → findings.
  • Check consistency between related columns, not just missing values.
  • Compare churn rate and lift by category; band numeric variables with cut/qcut.
  • Interactions (contract × tenure) reveal hot spots that single variables hide.
  • Quantify impact in money and end with specific, owned recommendations; remember that association is not causation.
Quick quiz6 questions · tap an answer to check it

1. What is "lift" in segment analysis?

Answer: B. Lift 2 = twice as likely as average.

2. Why band tenure into groups?

Answer: B. pd.cut for fixed edges, qcut for equal-sized groups.

3. Which finding came from an interaction analysis?

Answer: B. It combines two variables.

4. Why check total_charges against tenure × monthly charges?

Answer: A. Inconsistent columns hint at data errors.

5. What should an EDA conclude with?

Answer: B. Stakeholders act on findings, not charts.

6. Can EDA alone prove that a two-year contract reduces churn?

Answer: B. Self-selection can explain the pattern.
ExerciseExtend the case study

(1) Compute churn by payment method within month-to-month customers only. (2) Find the 10 highest-revenue customers at risk (month-to-month, tenure ≤ 12, 2+ support calls). (3) Plot churn rate against monthly charge deciles. (4) Write a five-bullet executive summary.

Show solution
Python
print(tc[tc.contract == "Month-to-month"].groupby("payment_method").churned.mean().round(3))
risk = tc[(tc.contract == "Month-to-month") & (tc.tenure_months <= 12) & (tc.support_calls >= 2) & (tc.churned == 0)]
print(risk.nlargest(10, "monthly_charges")[["customer_id", "monthly_charges", "support_calls"]])
dec = tc.groupby(pd.qcut(tc.monthly_charges, 10), observed=True).churned.mean()
dec.plot(marker="o")
Interview cornerQuestions you may be asked

Q: Walk me through how you would do EDA on a new dataset. Clarify the question and target, check quality (types, missing, duplicates, ranges, consistency), look at each variable, compare each variable against the target, explore interactions and segments, quantify impact, and summarise findings with next steps.

Q: How do you decide which variables matter most for churn? Compare churn rates/lift by category, correlations and group medians for numeric variables, then confirm with a model (feature importance) and, for causality, with experiments.

Q: Which chart would you use to show churn by two categorical variables? A heatmap of churn rates (with counts available) or grouped bars.

Practise more online

Free, trusted places to test yourself on this topic. Links open the original site in a new tab.