Mathew K Analytics

Lesson 49 · Market Research Analytics in Python

Retention and Churn Analysis Using Python for Market Research

Learn how to identify which customers will leave (churn) and what keeps them loyal. This analysis helps businesses improve customer loyalty, lower…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Retention and Churn Analysis in Market Research#

  • Learn how to identify which customers will leave (churn) and what keeps them loyal.
  • This analysis helps businesses improve customer loyalty, lower acquisition costs, and boost revenue.
  • We will uncover how to measure, visualize, and predict customer retention and churn, creating actionable business insights.
  • By the end, you will know how to find patterns, spot risks, and make recommendations to reduce churn.
import pandas as pd
import numpy as np
import openml
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

Core Market Research Concepts for Retention and Churn#

  • In retention and churn studies, data often includes customer demographics, behaviors, purchase history, and survey responses.
  • Each customer can have fields like gender, age, tenure, contract type, and whether they churned (left) or stayed.
  • Retention means a customer keeps using a company's product; churn means a customer left.
  • Beginners often forget to check for missing values, or mix up active customers with churned ones.
  • Another common mistake is not understanding what time windows the retention or churn rates cover (monthly, yearly, etc).
dataset = openml.datasets.get_dataset(42178)
df, _, _, _ = dataset.get_data(dataset_format='dataframe')
print(df.shape)
print(df.head(3))
(7043, 20)
   gender  SeniorCitizen Partner Dependents  tenure PhoneService  \
0  Female              0     Yes         No       1           No   
1    Male              0      No         No      34          Yes   
2    Male              0      No         No       2          Yes   

      MultipleLines InternetService OnlineSecurity OnlineBackup  \
0  No phone service             DSL             No          Yes   
1                No             DSL            Yes           No   
2                No             DSL            Yes          Yes   

  DeviceProtection TechSupport StreamingTV StreamingMovies        Contract  \
0               No          No          No              No  Month-to-month   
1              Yes          No          No              No        One year   
2               No          No          No              No  Month-to-month   

  PaperlessBilling     PaymentMethod  MonthlyCharges TotalCharges Churn  
0              Yes  Electronic check           29.85        29.85    No  
1               No      Mailed check           56.95       1889.5    No  
2              Yes      Mailed check           53.85       108.15   Yes  
print(df.columns)
print(df.dtypes)
Index(['gender', 'SeniorCitizen', 'Partner', 'Dependents', 'tenure',
       'PhoneService', 'MultipleLines', 'InternetService', 'OnlineSecurity',
       'OnlineBackup', 'DeviceProtection', 'TechSupport', 'StreamingTV',
       'StreamingMovies', 'Contract', 'PaperlessBilling', 'PaymentMethod',
       'MonthlyCharges', 'TotalCharges', 'Churn'],
      dtype='object')
gender               object
SeniorCitizen         uint8
Partner              object
Dependents           object
tenure                uint8
PhoneService         object
MultipleLines        object
InternetService      object
OnlineSecurity       object
OnlineBackup         object
DeviceProtection     object
TechSupport          object
StreamingTV          object
StreamingMovies      object
Contract             object
PaperlessBilling     object
PaymentMethod        object
MonthlyCharges      float64
TotalCharges         object
Churn                object
dtype: object
print(df['Churn'].value_counts())
Churn
No     5174
Yes    1869
Name: count, dtype: int64
churn_rate = df['Churn'].value_counts(normalize=True).get('Yes', 0) * 100
print(f'Overall churn rate: {churn_rate:.2f}%')
Overall churn rate: 26.54%
contract_churn = pd.crosstab(df['Contract'], df['Churn'], normalize='index')
print(contract_churn)
Churn                 No       Yes
Contract                          
Month-to-month  0.572903  0.427097
One year        0.887305  0.112695
Two year        0.971681  0.028319
contract_churn['Yes'].plot(kind='bar', color='salmon')
plt.ylabel('Churn Rate')
plt.title('Churn Rate by Contract Type')
plt.show()
No description has been provided for this image
gender_senior_churn = df.groupby(['gender','SeniorCitizen'])['Churn'].value_counts(normalize=True).unstack().fillna(0)
print(gender_senior_churn)
Churn                       No       Yes
gender SeniorCitizen                    
Female 0              0.760616  0.239384
       1              0.577465  0.422535
Male   0              0.767192  0.232808
       1              0.588850  0.411150
df['tenure_month'] = df['tenure']
monthly_churn = df.groupby('tenure_month')['Churn'].value_counts(normalize=True).unstack().fillna(0)['Yes']
monthly_churn.plot()
plt.xlabel('Customer Tenure (Months)')
plt.ylabel('Monthly Churn Rate')
plt.title('Monthly Churn Trend')
plt.show()
No description has been provided for this image
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
retail_df = pd.read_excel(url, sheet_name='Year 2010-2011')
retail_df['InvoiceDate'] = pd.to_datetime(retail_df['InvoiceDate'])
print(retail_df.shape)
print(retail_df.head(3))
(541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   

          InvoiceDate  Price  Customer ID         Country  
0 2010-12-01 08:26:00   2.55      17850.0  United Kingdom  
1 2010-12-01 08:26:00   3.39      17850.0  United Kingdom  
2 2010-12-01 08:26:00   2.75      17850.0  United Kingdom  
retail_df = retail_df.dropna(subset=['Customer ID'])
retail_df['CohortMonth'] = retail_df.groupby('Customer ID')['InvoiceDate'].transform('min').dt.to_period('M')
retail_df['InvoiceMonth'] = retail_df['InvoiceDate'].dt.to_period('M')
cohort_data = retail_df.groupby(['CohortMonth', 'InvoiceMonth'])['Customer ID'].nunique().reset_index()
cohort_pivot = cohort_data.pivot(index='CohortMonth', columns='InvoiceMonth', values='Customer ID')
retention = cohort_pivot.divide(cohort_pivot.iloc[:,0], axis=0)
print(retention.round(3).head())
InvoiceMonth  2010-12  2011-01  2011-02  2011-03  2011-04  2011-05  2011-06  \
CohortMonth                                                                   
2010-12           1.0    0.382    0.334    0.387     0.36    0.397     0.38   
2011-01           NaN      NaN      NaN      NaN      NaN      NaN      NaN   
2011-02           NaN      NaN      NaN      NaN      NaN      NaN      NaN   
2011-03           NaN      NaN      NaN      NaN      NaN      NaN      NaN   
2011-04           NaN      NaN      NaN      NaN      NaN      NaN      NaN   

InvoiceMonth  2011-07  2011-08  2011-09  2011-10  2011-11  2011-12  
CohortMonth                                                         
2010-12         0.354    0.354    0.395    0.373      0.5    0.274  
2011-01           NaN      NaN      NaN      NaN      NaN      NaN  
2011-02           NaN      NaN      NaN      NaN      NaN      NaN  
2011-03           NaN      NaN      NaN      NaN      NaN      NaN  
2011-04           NaN      NaN      NaN      NaN      NaN      NaN  
plt.figure(figsize=(12,6))
sns.heatmap(retention, annot=True, fmt='.0%', cmap='Blues')
plt.title('Customer Retention by Cohort Month')
plt.ylabel('Cohort Month')
plt.xlabel('Active Month')
plt.show()
No description has been provided for this image
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
churn_features = ['SeniorCitizen', 'tenure', 'MonthlyCharges']
X = df[churn_features]
y = df['Churn'].map({'No':0, 'Yes':1})
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))
              precision    recall  f1-score   support

           0       0.82      0.92      0.87      1539
           1       0.68      0.45      0.54       574

    accuracy                           0.79      2113
   macro avg       0.75      0.69      0.70      2113
weighted avg       0.78      0.79      0.78      2113

X['ChurnProb'] = model.predict_proba(X)[:,1]
threshold = 0.7
high_risk = df.loc[X['ChurnProb'] > threshold]
print(high_risk[['gender', 'SeniorCitizen', 'tenure', 'MonthlyCharges', 'Churn']].head())
     gender  SeniorCitizen  tenure  MonthlyCharges Churn
31     Male              1       2           95.50    No
91     Male              1       1           74.70    No
139  Female              1       1           70.45   Yes
171  Female              0       2          104.40   Yes
238  Female              1      11           95.00   Yes
missing_counts = df.isnull().sum()
print(missing_counts[missing_counts > 0])
Series([], dtype: int64)
try:
    print(df.groupby('Contract')['Churn'].value_counts())
except Exception as e:
    print('Error:', e)
Contract        Churn
Month-to-month  No       2220
                Yes      1655
One year        No       1307
                Yes       166
Two year        No       1647
                Yes        48
Name: count, dtype: int64
if 'NPS_Score' not in df.columns:
    print('NPS_Score column not present in this dataset. Skipping.')
else:
    nps_counts = df['NPS_Score'].value_counts().sort_index()
    print(nps_counts)
NPS_Score column not present in this dataset. Skipping.
segmentation = df.groupby(['gender','SeniorCitizen','Contract'])['Churn'].value_counts(normalize=True).unstack().fillna(0)['Yes']
print(segmentation.sort_values(ascending=False).head(10))
gender  SeniorCitizen  Contract      
Female  1              Month-to-month    0.553885
Male    1              Month-to-month    0.539216
Female  0              Month-to-month    0.406946
Male    0              Month-to-month    0.384565
Female  1              One year          0.158416
Male    1              One year          0.146067
        0              One year          0.117117
Female  0              One year          0.095624
        1              Two year          0.044118
Male    1              Two year          0.038961
Name: Yes, dtype: float64
payment_crosstab = pd.crosstab(df['PaymentMethod'], df['Churn'], normalize='index')
print(payment_crosstab)
Churn                            No       Yes
PaymentMethod                                
Bank transfer (automatic)  0.832902  0.167098
Credit card (automatic)    0.847569  0.152431
Electronic check           0.547146  0.452854
Mailed check               0.808933  0.191067
retention_index = (1 - churn_rate/100) * 100
print(f'Retention index (percentage who stay): {retention_index:.2f}%')
Retention index (percentage who stay): 73.46%
monthly_total = df.groupby('tenure_month').size()
monthly_retained = df[df['Churn'] == 'No'].groupby('tenure_month').size()
retention_trend = (monthly_retained / monthly_total * 100).fillna(0)
retention_trend.plot()
plt.ylabel('Retention Rate (%)')
plt.xlabel('Tenure Month')
plt.title('Customer Retention Over Tenure')
plt.show()
No description has been provided for this image

End-to-End Market Research Example: Actionable Churn Recommendation#

  • Now combine your knowledge by running a full churn analysis and making a concrete recommendation.
  • We will go from raw data to a final business insight.
  • Your goal: Identify and briefly justify what customer segment needs the most urgent retention effort.
# 1. Load data and show basic churn statistics
print(df['Churn'].value_counts(normalize=True) * 100)
Churn
No     73.463013
Yes    26.536987
Name: proportion, dtype: float64
# 2. Churn by contract type and payment method, then find high-risk segments
risk_segments = pd.crosstab([df['Contract'], df['PaymentMethod']], df['Churn'], normalize='index').reset_index()
top_risks = risk_segments.sort_values('Yes', ascending=False).head(5)
print(top_risks[['Contract', 'PaymentMethod', 'Yes']])
Churn        Contract              PaymentMethod       Yes
2      Month-to-month           Electronic check  0.537297
0      Month-to-month  Bank transfer (automatic)  0.341256
1      Month-to-month    Credit card (automatic)  0.327808
3      Month-to-month               Mailed check  0.315789
6            One year           Electronic check  0.184438
 

Found this useful?

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