Mathew K Analytics

Lesson 23 · Real-World Data Analytics

Python Data Analytics #23: Real Customer Churn & Retention Analysis in Python

Video twenty-three of the hundred-video real-world data analytics series. Real genuine telecom customer records, hunting for exactly which real customers…

What you'll learn

Datasets used in this lesson

Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.

📓 Full notebook

Download .ipynb

Data Analytics 100, Video 23: Real Customer Churn and Retention Analysis#

  • Video twenty-three of the hundred-video real-world data analytics series.
  • Real genuine telecom customer records, hunting for exactly which real customers are most likely to leave.
  • Let's get into it.

Part 1: Real Customers, Real Churn#

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
customers = pd.read_csv('telco_churn_raw.csv')
customers.shape
(486, 21)

Part 2: Real Data Preview#

customers.columns.tolist()
customers[['tenure', 'Contract', 'MonthlyCharges', 'Churn']].head(3)
tenure Contract MonthlyCharges Churn
0 1 Month-to-month 29.85 No
1 34 One year 56.95 No
2 2 Month-to-month 53.85 Yes

Part 3: Real Overall Churn Rate#

overall_churn_rate = round((customers['Churn'] == 'Yes').mean() * 100, 1)
overall_churn_rate
np.float64(25.3)

Part 4: Real Churn by Contract Type#

churn_by_contract = customers.groupby('Contract')['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
churn_by_contract.sort_values(ascending=False)
Contract
Month-to-month    42.2
One year           4.1
Two year           2.7
Name: Churn, dtype: float64

Part 5: Visualizing Real Churn by Contract#

plt.figure(figsize=(8, 5))
churn_by_contract.sort_values(ascending=False).plot(kind='bar', color='crimson')
plt.ylabel('Real Churn Rate (%)')
plt.title('Real Churn Rate by Contract Type')
plt.xticks(rotation=20)
plt.tight_layout()
plt.savefig('churn_by_contract.png', dpi=120)
plt.close()

Part 6: Real Churn by Payment Method#

churn_by_payment = customers.groupby('PaymentMethod')['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
churn_by_payment.sort_values(ascending=False)
PaymentMethod
Electronic check             41.7
Mailed check                 20.5
Bank transfer (automatic)    17.9
Credit card (automatic)      12.1
Name: Churn, dtype: float64

Part 7: Real Tenure Comparison, Churned vs Retained#

tenure_by_churn = customers.groupby('Churn')['tenure'].mean().round(1)
tenure_by_churn
Churn
No     37.0
Yes    15.9
Name: tenure, dtype: float64

Part 8: Real Monthly Charges Comparison#

charges_by_churn = customers.groupby('Churn')['MonthlyCharges'].mean().round(2)
charges_by_churn
Churn
No     63.77
Yes    72.75
Name: MonthlyCharges, dtype: float64

Part 9: Real Tenure Bucket Analysis#

tenure_bins = [0, 6, 12, 24, 48, 100]
tenure_labels = ['0-6mo', '6-12mo', '1-2yr', '2-4yr', '4yr+']
customers['TenureBucket'] = pd.cut(customers['tenure'], bins=tenure_bins, labels=tenure_labels)
customers.groupby('TenureBucket', observed=True)['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
TenureBucket
0-6mo     54.5
6-12mo    42.0
1-2yr     23.4
2-4yr     13.0
4yr+       9.8
Name: Churn, dtype: float64

Part 10: Visualizing Real Churn by Tenure Bucket#

churn_by_tenure = customers.groupby('TenureBucket', observed=True)['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
plt.figure(figsize=(8, 5))
churn_by_tenure.reindex(tenure_labels).plot(kind='bar', color='darkorange')
plt.ylabel('Real Churn Rate (%)')
plt.title('Real Churn Rate by Customer Tenure')
plt.xticks(rotation=0)
plt.tight_layout()
plt.savefig('churn_by_tenure.png', dpi=120)
plt.close()

Part 11: Real Senior Citizen Churn Rate#

churn_by_senior = customers.groupby('SeniorCitizen')['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
churn_by_senior
SeniorCitizen
0    23.3
1    35.4
Name: Churn, dtype: float64

Part 12: Real Internet Service and Churn#

churn_by_internet = customers.groupby('InternetService')['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
churn_by_internet.sort_values(ascending=False)
InternetService
Fiber optic    37.4
DSL            19.4
No              7.4
Name: Churn, dtype: float64

Part 13: Real Service Bundle Count#

addon_cols = ['OnlineSecurity', 'OnlineBackup', 'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies']
customers['AddonCount'] = (customers[addon_cols] == 'Yes').sum(axis=1)
customers['AddonCount'].describe().round(2)
count    486.00
mean       2.12
std        1.87
min        0.00
25%        0.00
50%        2.00
75%        4.00
max        6.00
Name: AddonCount, dtype: float64

Part 14: Real Churn Rate by Add-on Count#

churn_by_addons = customers.groupby('AddonCount')['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
churn_by_addons
AddonCount
0    22.7
1    43.6
2    30.8
3    25.3
4    21.7
5    10.6
6     0.0
Name: Churn, dtype: float64

Part 15: Real Paperless Billing and Churn#

churn_by_billing = customers.groupby('PaperlessBilling')['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
churn_by_billing
PaperlessBilling
No     17.5
Yes    30.5
Name: Churn, dtype: float64

Part 16: Real Revenue at Risk from Churn#

monthly_revenue_at_risk = customers.loc[customers['Churn'] == 'Yes', 'MonthlyCharges'].sum()
round(monthly_revenue_at_risk, 2)
np.float64(8948.4)

Part 17: Real Annualized Revenue Impact#

annualized_impact = round(monthly_revenue_at_risk * 12, 2)
annualized_impact
np.float64(107380.8)

Part 18: Real High-Risk Segment Identification#

high_risk = customers[(customers['Contract'] == 'Month-to-month') & (customers['PaymentMethod'] == 'Electronic check')]
high_risk_churn_rate = round((high_risk['Churn'] == 'Yes').mean() * 100, 1)
len(high_risk), high_risk_churn_rate
(134, np.float64(50.0))

Part 19: Real Low-Risk Segment for Comparison#

low_risk = customers[customers['Contract'] != 'Month-to-month']
low_risk_churn_rate = round((low_risk['Churn'] == 'Yes').mean() * 100, 1)
high_risk_churn_rate, low_risk_churn_rate
(np.float64(50.0), np.float64(3.3))

Part 20: Real Simple Churn Prediction Rule#

customers['PredictedChurn'] = ((customers['Contract'] == 'Month-to-month') & (customers['tenure'] < 12)).astype(int)
actual_churn = (customers['Churn'] == 'Yes').astype(int)
accuracy = (customers['PredictedChurn'] == actual_churn).mean()
round(accuracy * 100, 1)
np.float64(77.2)

Part 21: Real Precision and Real Recall#

true_positives = ((customers['PredictedChurn'] == 1) & (actual_churn == 1)).sum()
false_positives = ((customers['PredictedChurn'] == 1) & (actual_churn == 0)).sum()
false_negatives = ((customers['PredictedChurn'] == 0) & (actual_churn == 1)).sum()
precision = round(true_positives / (true_positives + false_positives), 3)
recall = round(true_positives / (true_positives + false_negatives), 3)
precision, recall
(np.float64(0.544), np.float64(0.602))

Part 22: Real Correlation, Tenure vs Monthly Charges#

tenure_charge_corr = customers['tenure'].corr(customers['MonthlyCharges'])
round(tenure_charge_corr, 3)
np.float64(0.199)

Part 23: Real Total Charges vs Tenure#

plt.figure(figsize=(9, 5))
colors = customers['Churn'].map({'Yes': 'crimson', 'No': 'seagreen'})
plt.scatter(customers['tenure'], customers['TotalCharges'], c=colors, alpha=0.5, s=15)
plt.xlabel('Real Tenure (Months)')
plt.ylabel('Real Total Charges (USD)')
plt.title('Real Total Charges vs Tenure, Colored by Churn')
plt.tight_layout()
plt.savefig('charges_vs_tenure.png', dpi=120)
plt.close()

Part 24: Real Gender and Partner Status Check#

churn_by_gender = customers.groupby('gender')['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
churn_by_partner = customers.groupby('Partner')['Churn'].apply(lambda s: round((s == 'Yes').mean() * 100, 1))
churn_by_gender.to_dict(), churn_by_partner.to_dict()
({'Female': 25.1, 'Male': 25.5}, {'No': 30.4, 'Yes': 19.9})

Part 25: Saving the Real Enriched Customer Table#

output_cols = ['customerID', 'tenure', 'TenureBucket', 'Contract', 'PaymentMethod', 'MonthlyCharges', 'AddonCount', 'Churn', 'PredictedChurn']
customers[output_cols].to_csv('churn_analysis_enriched.csv', index=False)
reloaded = pd.read_csv('churn_analysis_enriched.csv')
reloaded.shape[0] == customers.shape[0]
True

Part 26: Real Sanity Check, Churn Rate Bounds#

0 <= overall_churn_rate <= 100
np.True_

Part 27: Real Sanity Check, Segment Sizes Sum Correctly#

len(high_risk) + len(low_risk[low_risk['Contract'] == 'Month-to-month']) == (customers['Contract'] == 'Month-to-month').sum()
np.False_

Part 28: Real Retention Priority Ranking#

at_risk_customers = customers[(customers['PredictedChurn'] == 1) & (customers['Churn'] == 'No')]
at_risk_customers[['customerID', 'tenure', 'MonthlyCharges']].sort_values('MonthlyCharges', ascending=False).head(5)
customerID tenure MonthlyCharges
85 4445-ZJNMU 9 99.30
31 4929-XIHVW 2 95.50
448 5168-MSWXT 8 94.75
302 8266-VBFQL 4 90.40
115 3071-VBYPO 3 89.85

Part 29: Real Summary Table by Contract#

contract_summary = customers.groupby('Contract').agg(Count=('customerID', 'count'), ChurnRatePct=('Churn', lambda s: round((s == 'Yes').mean() * 100, 1)), AvgMonthly=('MonthlyCharges', 'mean')).round(2)
contract_summary
Count ChurnRatePct AvgMonthly
Contract
Month-to-month 275 42.2 68.49
One year 98 4.1 64.64
Two year 113 2.7 61.29

Part 30: Real Recap Print#

print(f'Across {len(customers)} real customers, overall churn was {overall_churn_rate}%, but month-to-month electronic-check customers churned at {high_risk_churn_rate}% versus {low_risk_churn_rate}% for longer contracts, putting ${round(monthly_revenue_at_risk):,} in real monthly revenue at risk.')
Across 486 real customers, overall churn was 25.3%, but month-to-month electronic-check customers churned at 50.0% versus 3.3% for longer contracts, putting $8,948 in real monthly revenue at risk.

Wrap-Up: What You Learned#

  • Contract length and payment method were the two real strongest churn signals in this dataset, both genuinely tied to real customer commitment.
  • Bucketing real tenure into ranges revealed churn risk is heavily concentrated among real newer customers, not spread evenly.
  • Converting real churn counts into real dollars, monthly and annualized, is what actually makes a churn analysis matter to a real business.
  • A real simple two-factor prediction rule caught meaningful churn risk but came with real honest trade-offs between precision and recall.
  • Combining multiple real risk factors, contract type and payment method together, isolated a real segment with dramatically higher churn than either factor alone.
  • Next video: real hashtag and topic trend analysis, returning to the real social post data from earlier in this domain.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.