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…
- CourseReal-World Data Analytics
- Lesson23 of 100
- Video27 min
- FormatJupyter notebook · 30 code cells
- Data1 dataset
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.
- telco_churn_raw.csv64.8 KB
📓 Full notebook
Download .ipynbData 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
Part 2: Real Data Preview#
customers.columns.tolist()
customers[['tenure', 'Contract', 'MonthlyCharges', 'Churn']].head(3)
Part 3: Real Overall Churn Rate#
overall_churn_rate = round((customers['Churn'] == 'Yes').mean() * 100, 1)
overall_churn_rate
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)
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)
Part 7: Real Tenure Comparison, Churned vs Retained#
tenure_by_churn = customers.groupby('Churn')['tenure'].mean().round(1)
tenure_by_churn
Part 8: Real Monthly Charges Comparison#
charges_by_churn = customers.groupby('Churn')['MonthlyCharges'].mean().round(2)
charges_by_churn
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))
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
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)
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)
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
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
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)
Part 17: Real Annualized Revenue Impact#
annualized_impact = round(monthly_revenue_at_risk * 12, 2)
annualized_impact
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
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
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)
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
Part 22: Real Correlation, Tenure vs Monthly Charges#
tenure_charge_corr = customers['tenure'].corr(customers['MonthlyCharges'])
round(tenure_charge_corr, 3)
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()
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]
Part 26: Real Sanity Check, Churn Rate Bounds#
0 <= overall_churn_rate <= 100
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()
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)
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
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.')
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.



