In [6]:
#import libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import sqlite3
In [8]:
# 1. import database
print("Hello")
Hello
In [9]:
conn = sqlite3.connect(
r'C:\Users\Hp\Downloads\customer_churn.db'
)
sql_query = """
SELECT name
FROM sqlite_master
WHERE type ='table'
"""
tables = pd.read_sql(sql_query, conn)
#create dataframe for each table
for table_name in tables['name']:
df = pd.read_sql(f"SELECT * FROM {table_name}",conn)
globals()[f"df_{table_name}"] = df
print(f"Created dataframe: df_{table_name}")
conn.close()
print(tables)
Created dataframe: df_db_customer
Created dataframe: df_db_subscription
Created dataframe: df_db_support
name
0 db_customer
1 db_subscription
2 db_support
In [20]:
#print table names and cloumns name
conn = sqlite3.connect(
r'C:\Users\Hp\Downloads\customer_churn.db'
)
for table_name in tables['name']:
print(f"\nTable Name: {table_name}")
#get column information
columns_query = f"PRAGMA table_info({table_name});"
columns = pd.read_sql(columns_query, conn)
print("Columns:")
print (columns['name'].tolist())
conn.close()
Table Name: db_customer Columns: ['customerid', 'name', 'country', 'state', 'gender', 'dob', 'interests', 'pincode'] Table Name: db_subscription Columns: ['customerid', 'subscription_start_date', 'subscription_type', 'renewal_date', 'plan_type', 'contract_type', 'cancellation_date', 'cancellation_reason', 'monthly_charges', 'cltv', 'churn_score'] Table Name: db_support Columns: ['customerid', 'complaint_date', 'escalations', 'csat_score', 'col_1', 'comment']
In [21]:
# 2. data cleaning
In [22]:
df_db_customer.head()
#df_db_customer.tail()
Out[22]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>customer_name</th>
<th>country</th>
<th>state</th>
<th>gender</th>
<th>dob</th>
<th>interests</th>
<th>pincode</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| 0002-ORFBO | keshav | India | Maharashtra | Male | 1982-04-12 00:00:00 | travel | None |
| 0003-MKNFE | raghav | India | Karnataka | Male | 1995-11-23 00:00:00 | NaN | None |
| 0004-TLHLJ | lalita | India | Delhi | Female | 1978-02-15 00:00:00 | movie | None |
| 0011-IGKFF | mohan | India | Nagaland | Male | 2001-08-30 00:00:00 | NaN | None |
| 0013-EXCHZ | mira | India | Delhi | Female | 1990-05-05 00:00:00 | drama | None |
In [23]:
df_db_customer.info()
<class 'pandas.DataFrame'> RangeIndex: 21 entries, 0 to 20 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerid 21 non-null str 1 customer_name 21 non-null str 2 country 18 non-null str 3 state 21 non-null str 4 gender 21 non-null str 5 dob 21 non-null str 6 interests 4 non-null str 7 pincode 0 non-null object dtypes: object(1), str(7) memory usage: 1.4+ KB
In [24]:
# a. drop columns interest and pincode
# b. rename col - name
# c. change the data type of date
# d. fix missing values - country
# e. data standardization - gender
In [25]:
# a. renmae col - name
df_db_customer.rename(columns = {'name': 'customer_name'}, inplace = True)
df_db_customer
Out[25]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>customer_name</th>
<th>country</th>
<th>state</th>
<th>gender</th>
<th>dob</th>
<th>interests</th>
<th>pincode</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9</th>
<th>10</th>
<th>11</th>
<th>12</th>
<th>13</th>
<th>14</th>
<th>15</th>
<th>16</th>
<th>17</th>
<th>18</th>
<th>19</th>
<th>20</th>
</tbody>
<tbody>
| 0002-ORFBO | keshav | India | Maharashtra | Male | 1982-04-12 00:00:00 | travel | None |
| 0003-MKNFE | raghav | India | Karnataka | Male | 1995-11-23 00:00:00 | NaN | None |
| 0004-TLHLJ | lalita | India | Delhi | Female | 1978-02-15 00:00:00 | movie | None |
| 0011-IGKFF | mohan | India | Nagaland | Male | 2001-08-30 00:00:00 | NaN | None |
| 0013-EXCHZ | mira | India | Delhi | Female | 1990-05-05 00:00:00 | drama | None |
| 0013-MHZWF | durga | NaN | Delhi | Women | 1988-12-10 00:00:00 | NaN | None |
| 0013-SMEOE | mina | India | Meghalaya | Female | 1976-09-21 00:00:00 | NaN | None |
| 0014-BMAQU | madan | India | Rajasthan | Male | 1999-03-14 00:00:00 | NaN | None |
| 0015-UOCOJ | maya | NaN | Kathmandu | Women | 1985-07-07 00:00:00 | NaN | None |
| 0016-QLJIS | arjun | Nepal | Kathmandu | Male | 1993-10-29 00:00:00 | NaN | None |
| 0017-DINOC | shiva | India | Maharashtra | Men | 1997-01-22 00:00:00 | NaN | None |
| 0017-IUDMW | rangadevi | India | Karnataka | Female | 1981-06-18 00:00:00 | NaN | None |
| 0018-NYROU | chitra | NaN | Telangana | Female | 2004-12-01 00:00:00 | NaN | None |
| 0019-EFAEP | raju | India | Meghalaya | Female | 1992-04-25 00:00:00 | NaN | None |
| 0019-GFNTW | Madhav | India | Uttar Pradesh | Men | 1979-11-11 00:00:00 | NaN | None |
| 0020-INWCK | parvati | India | Delhi | Female | 1986-02-28 00:00:00 | job | None |
| 0020-JDNXP | rikim | India | Meghalaya | Female | 1994-08-19 00:00:00 | NaN | None |
| 0021-IKXGC | vishakha | India | Rajasthan | Female | 2000-09-02 00:00:00 | NaN | None |
| 0022-TCJCI | raghvendra | India | Telangana | Male | 1983-12-30 00:00:00 | NaN | None |
| 0023-HGHWL | rishabh | India | Uttar Pradesh | Men | 1991-05-14 00:00:00 | NaN | None |
| 0023-UYUPN | sudevi | India | Maharashtra | Women | 1977-10-06 00:00:00 | NaN | None |
In [26]:
#b. drop columns interest and pincode
df_db_customer.drop(columns = ['interests','pincode'] , inplace = True ,errors='ignore')
In [27]:
df_db_customer.info()
<class 'pandas.DataFrame'> RangeIndex: 21 entries, 0 to 20 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerid 21 non-null str 1 customer_name 21 non-null str 2 country 18 non-null str 3 state 21 non-null str 4 gender 21 non-null str 5 dob 21 non-null str dtypes: str(6) memory usage: 1.1 KB
In [28]:
#c. change the date datatype
df_db_customer['dob'] = pd.to_datetime(df_db_customer['dob'])
In [29]:
# Convert selected columns to object
cols_to_object = [
'customerid',
'customer_name',
'country',
'state',
'gender'
]
df_db_customer[cols_to_object] = df_db_customer[cols_to_object].astype('object')
# Convert dob to datetime
df_db_customer['dob'] = pd.to_datetime(df_db_customer['dob'])
In [30]:
df_db_customer.info()
<class 'pandas.DataFrame'> RangeIndex: 21 entries, 0 to 20 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerid 21 non-null object 1 customer_name 21 non-null object 2 country 18 non-null object 3 state 21 non-null object 4 gender 21 non-null object 5 dob 21 non-null datetime64[us] dtypes: datetime64[us](1), object(5) memory usage: 1.1+ KB
In [31]:
# d. standardization
df_db_customer['gender'].unique()
df_db_customer['gender'] = df_db_customer['gender'].replace({'Men' : 'Male' , 'Women' : 'Female'})
In [32]:
# e. missing values fixing
df_db_customer[df_db_customer['country'].isna()]
Out[32]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>customer_name</th>
<th>country</th>
<th>state</th>
<th>gender</th>
<th>dob</th>
</thead>
<tbody>
<th>5</th>
<th>8</th>
<th>12</th>
</tbody>
<tbody>
| 0013-MHZWF | durga | NaN | Delhi | Female | 1988-12-10 |
| 0015-UOCOJ | maya | NaN | Kathmandu | Female | 1985-07-07 |
| 0018-NYROU | chitra | NaN | Telangana | Female | 2004-12-01 |
In [33]:
#df_db_customer[['country','state']]
#country and state unique value pair
state_country_mapping = df_db_customer.dropna(subset = ['country']).set_index('state')['country'].to_dict()
df_db_customer['country'] = df_db_customer['country'].fillna(df_db_customer['state'].map(state_country_mapping))
In [34]:
df_db_customer[df_db_customer['country'].isna()]
Out[34]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>customer_name</th>
<th>country</th>
<th>state</th>
<th>gender</th>
<th>dob</th>
</thead>
<tbody>
</tbody>
<tbody>
In [35]:
df_db_subscription.head()
Out[35]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>subscription_start_date</th>
<th>subscription_type</th>
<th>renewal_date</th>
<th>plan_type</th>
<th>contract_type</th>
<th>cancellation_date</th>
<th>cancellation_reason</th>
<th>monthly_charges</th>
<th>cltv</th>
<th>churn_score</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| 0002-ORFBO | 2021-03-15 | Refferal | 2025-03-15 | Standard | Annual | NaN | NaN | 13.99 | 627 | 12 |
| 0003-MKNFE | 2020-08-01 | Paid | 2024-08-01 | Premium | Annual | 2024-09-10 | Switched to competitor | 12.99 | 1150 | 91 |
| 0004-TLHLJ | 2022-11-20 | Organic | 2025-11-20 | Basic | Monthly | NaN | NaN | 6.99 | 210 | 34 |
| 0011-IGKFF | 2019-05-10 | Paid | 2025-05-10 | Premium | Annual | NaN | NaN | 22.99 | 1725 | 8 |
| 0013-EXCHZ | 2023-01-05 | Refferal | 2024-01-05 | Standard | Monthly | 2024-02-28 | Too expensive | 13.99 | 195 | 88 |
In [36]:
df_db_subscription.info()
<class 'pandas.DataFrame'> RangeIndex: 21 entries, 0 to 20 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerid 21 non-null str 1 subscription_start_date 21 non-null str 2 subscription_type 21 non-null str 3 renewal_date 21 non-null str 4 plan_type 21 non-null str 5 contract_type 21 non-null str 6 cancellation_date 6 non-null str 7 cancellation_reason 6 non-null str 8 monthly_charges 21 non-null float64 9 cltv 21 non-null int64 10 churn_score 21 non-null int64 dtypes: float64(1), int64(2), str(8) memory usage: 1.9 KB
In [37]:
# Convert all string columns to object first
str_cols = df_db_subscription.select_dtypes(include='string').columns
df_db_subscription[str_cols] = df_db_subscription[str_cols].astype('object')
# Convert date columns to datetime
date_cols = [
'subscription_start_date',
'renewal_date',
'cancellation_date'
]
for col in date_cols:
df_db_subscription[col] = pd.to_datetime(
df_db_subscription[col],
errors='coerce'
)
In [38]:
df_db_subscription.info()
<class 'pandas.DataFrame'> RangeIndex: 21 entries, 0 to 20 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerid 21 non-null object 1 subscription_start_date 21 non-null datetime64[us] 2 subscription_type 21 non-null object 3 renewal_date 21 non-null datetime64[us] 4 plan_type 21 non-null object 5 contract_type 21 non-null object 6 cancellation_date 6 non-null datetime64[us] 7 cancellation_reason 6 non-null object 8 monthly_charges 21 non-null float64 9 cltv 21 non-null int64 10 churn_score 21 non-null int64 dtypes: datetime64[us](3), float64(1), int64(2), object(5) memory usage: 1.9+ KB
In [39]:
df_db_support.head()
Out[39]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>complaint_date</th>
<th>escalations</th>
<th>csat_score</th>
<th>col_1</th>
<th>comment</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| 0003-MKNFE | 2024-08-28 00:00:00 | N | 60 | None | service issue |
| 0003-MKNFE | 2024-08-28 00:00:00 | Y | 10 | None | demaned refund |
| 0013-EXCHZ | 2024-01-20 00:00:00 | Y | 20 | None | NaN |
| 0013-MHZWF | 2025-03-18 00:00:00 | N | 90 | None | guidance to renew |
| 0013-SMEOE | 2024-11-01 00:00:00 | N | 30 | None | NaN |
In [40]:
df_db_support.info()
<class 'pandas.DataFrame'> RangeIndex: 9 entries, 0 to 8 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerid 9 non-null str 1 complaint_date 9 non-null str 2 escalations 9 non-null str 3 csat_score 9 non-null int64 4 col_1 0 non-null object 5 comment 4 non-null str dtypes: int64(1), object(1), str(4) memory usage: 564.0+ bytes
In [41]:
df_db_support.drop(
columns=['col_1', 'comment'],
inplace=True,
errors='ignore'
)
In [42]:
df_db_support.info()
<class 'pandas.DataFrame'> RangeIndex: 9 entries, 0 to 8 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerid 9 non-null str 1 complaint_date 9 non-null str 2 escalations 9 non-null str 3 csat_score 9 non-null int64 dtypes: int64(1), str(3) memory usage: 420.0 bytes
In [43]:
# Convert all string columns to object
str_cols = df_db_support.select_dtypes(include='string').columns
df_db_support[str_cols] = df_db_support[str_cols].astype('object')
# Convert complaint_date to datetime
df_db_support['complaint_date'] = pd.to_datetime(
df_db_support['complaint_date'],
errors='coerce'
)
In [44]:
df_db_support.info()
<class 'pandas.DataFrame'> RangeIndex: 9 entries, 0 to 8 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customerid 9 non-null object 1 complaint_date 9 non-null datetime64[us] 2 escalations 9 non-null object 3 csat_score 9 non-null int64 dtypes: datetime64[us](1), int64(1), object(2) memory usage: 420.0+ bytes
In [45]:
# 3. Featrure Enginerring and data analysis
In [46]:
#create a new cloumn using existing col
df_db_subscription['churn_flag'] = np.where(df_db_subscription['cancellation_date'].notna(),1,0)
In [47]:
df_db_subscription.head()
Out[47]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>subscription_start_date</th>
<th>subscription_type</th>
<th>renewal_date</th>
<th>plan_type</th>
<th>contract_type</th>
<th>cancellation_date</th>
<th>cancellation_reason</th>
<th>monthly_charges</th>
<th>cltv</th>
<th>churn_score</th>
<th>churn_flag</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| 0002-ORFBO | 2021-03-15 | Refferal | 2025-03-15 | Standard | Annual | NaT | NaN | 13.99 | 627 | 12 | 0 |
| 0003-MKNFE | 2020-08-01 | Paid | 2024-08-01 | Premium | Annual | 2024-09-10 | Switched to competitor | 12.99 | 1150 | 91 | 1 |
| 0004-TLHLJ | 2022-11-20 | Organic | 2025-11-20 | Basic | Monthly | NaT | NaN | 6.99 | 210 | 34 | 0 |
| 0011-IGKFF | 2019-05-10 | Paid | 2025-05-10 | Premium | Annual | NaT | NaN | 22.99 | 1725 | 8 | 0 |
| 0013-EXCHZ | 2023-01-05 | Refferal | 2024-01-05 | Standard | Monthly | 2024-02-28 | Too expensive | 13.99 | 195 | 88 | 1 |
In [48]:
df = (df_db_subscription
.merge(df_db_customer , on ='customerid' , how = 'left')
.merge(df_db_support, on ='customerid' , how = 'left'))
In [49]:
df.shape
Out[49]:
(23, 20)
In [50]:
df_db_subscription['customerid'].nunique()
Out[50]:
21
In [51]:
df_db_customer['customerid'].nunique()
Out[51]:
21
In [52]:
df_db_support['customerid'].nunique()
Out[52]:
7
In [53]:
df_db_support
Out[53]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>complaint_date</th>
<th>escalations</th>
<th>csat_score</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
</tbody>
<tbody>
| 0003-MKNFE | 2024-08-28 | N | 60 |
| 0003-MKNFE | 2024-08-28 | Y | 10 |
| 0013-EXCHZ | 2024-01-20 | Y | 20 |
| 0013-MHZWF | 2025-03-18 | N | 90 |
| 0013-SMEOE | 2024-11-01 | N | 30 |
| 0017-IUDMW | 2024-04-10 | Y | 25 |
| 0019-EFAEP | 2024-09-27 | Y | 30 |
| 0022-TCJCI | 2024-09-13 | Y | 10 |
| 0022-TCJCI | 2024-09-14 | N | 90 |
In [54]:
#as it has repeated entries we will on consider those which has last occurance
df_db_support['complaint_count'] = df_db_support.groupby('customerid')['customerid'].transform('count')
In [55]:
df_db_support = df_db_support.sort_values('complaint_date').drop_duplicates('customerid',keep ='last')
In [56]:
df_db_support['customerid'].size
Out[56]:
7
In [57]:
#merge df
df = (df_db_subscription
.merge(df_db_customer, on = 'customerid' , how ='left')
.merge(df_db_support, on = 'customerid' , how = 'left'))
In [58]:
df.shape
Out[58]:
(21, 21)
In [59]:
df.to_csv('exported_churn_data.csv',index=False)
In [ ]:
In [60]:
import os
print(os.getcwd())
C:\Users\Hp
In [61]:
print(os.path.abspath('exported_churn_data.csv'))
C:\Users\Hp\exported_churn_data.csv
In [ ]:
In [62]:
# DATA ANALYSIS
#1. charn rate
In [63]:
df.columns
Out[63]:
Index(['customerid', 'subscription_start_date', 'subscription_type',
'renewal_date', 'plan_type', 'contract_type', 'cancellation_date',
'cancellation_reason', 'monthly_charges', 'cltv', 'churn_score',
'churn_flag', 'customer_name', 'country', 'state', 'gender', 'dob',
'complaint_date', 'escalations', 'csat_score', 'complaint_count'],
dtype='str')
In [64]:
churn_rate = df['churn_flag'].mean()*100
print("churn Rate is : ",round(churn_rate,2),'%')
churn Rate is : 28.57 %
In [65]:
#2. Retantion rate
retention_rate = 100-churn_rate
print("Retention Rate is : ",round(retention_rate,2),'%')
Retention Rate is : 71.43 %
In [66]:
df.head(2)
Out[66]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>subscription_start_date</th>
<th>subscription_type</th>
<th>renewal_date</th>
<th>plan_type</th>
<th>contract_type</th>
<th>cancellation_date</th>
<th>cancellation_reason</th>
<th>monthly_charges</th>
<th>cltv</th>
<th>...</th>
<th>churn_flag</th>
<th>customer_name</th>
<th>country</th>
<th>state</th>
<th>gender</th>
<th>dob</th>
<th>complaint_date</th>
<th>escalations</th>
<th>csat_score</th>
<th>complaint_count</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
</tbody>
<tbody>
<p>2 rows × 21 columns</p>
| 0002-ORFBO | 2021-03-15 | Refferal | 2025-03-15 | Standard | Annual | NaT | NaN | 13.99 | 627 | ... | 0 | keshav | India | Maharashtra | Male | 1982-04-12 | NaT | NaN | NaN | NaN |
| 0003-MKNFE | 2020-08-01 | Paid | 2024-08-01 | Premium | Annual | 2024-09-10 | Switched to competitor | 12.99 | 1150 | ... | 1 | raghav | India | Karnataka | Male | 1995-11-23 | 2024-08-28 | Y | 10.0 | 2.0 |
In [67]:
# 3. churn by plan type
churn_by_plan = df.groupby('plan_type')['churn_flag'].mean().mul(100).round(2).reset_index(name ='churn_rate_percentage')
print(churn_by_plan)
plan_type churn_rate_percentage 0 Basic 60.00 1 Premium 14.29 2 Standard 22.22
In [68]:
# 4 a. churn by state + sum(revenue) & count of users
churn_by_state = (
df.groupby('state')
.agg(
total_revenue=('monthly_charges', 'sum'),
user_count=('customerid', 'nunique'),
churned_users=('churn_flag', 'sum')
)
.reset_index()
)
churn_by_state
Out[68]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>state</th>
<th>total_revenue</th>
<th>user_count</th>
<th>churned_users</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
</tbody>
<tbody>
| Delhi | 52.96 | 4 | 1 |
| Karnataka | 20.98 | 2 | 2 |
| Kathmandu | 20.98 | 2 | 0 |
| Maharashtra | 50.97 | 3 | 0 |
| Meghalaya | 42.97 | 3 | 2 |
| Nagaland | 22.99 | 1 | 0 |
| Rajasthan | 36.98 | 2 | 0 |
| Telangana | 30.98 | 2 | 1 |
| Uttar Pradesh | 115.98 | 2 | 0 |
In [69]:
churn_by_state['churn_rate'] = (
churn_by_state['churned_users']
/ churn_by_state['user_count']
* 100
)
churn_by_state
Out[69]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>state</th>
<th>total_revenue</th>
<th>user_count</th>
<th>churned_users</th>
<th>churn_rate</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
</tbody>
<tbody>
| Delhi | 52.96 | 4 | 1 | 25.000000 |
| Karnataka | 20.98 | 2 | 2 | 100.000000 |
| Kathmandu | 20.98 | 2 | 0 | 0.000000 |
| Maharashtra | 50.97 | 3 | 0 | 0.000000 |
| Meghalaya | 42.97 | 3 | 2 | 66.666667 |
| Nagaland | 22.99 | 1 | 0 | 0.000000 |
| Rajasthan | 36.98 | 2 | 0 | 0.000000 |
| Telangana | 30.98 | 2 | 1 | 50.000000 |
| Uttar Pradesh | 115.98 | 2 | 0 | 0.000000 |
In [70]:
# 4b .churn by subscription type + sum(revenue) & count of users
churn_by_subscription = (
df.groupby('subscription_type')
.agg(
total_revenue=('monthly_charges', 'sum'),
user_count=('customerid', 'nunique'),
churned_users=('churn_flag', 'sum')
)
.reset_index()
)
churn_by_subscription
Out[70]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>subscription_type</th>
<th>total_revenue</th>
<th>user_count</th>
<th>churned_users</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
</tbody>
<tbody>
| Organic | 145.91 | 9 | 0 |
| Paid | 174.94 | 6 | 1 |
| Refferal | 74.94 | 6 | 5 |
In [71]:
churn_by_subscription['churn_rate'] = (
churn_by_subscription['churned_users']
/ churn_by_subscription['user_count']
* 100
)
churn_by_subscription
Out[71]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>subscription_type</th>
<th>total_revenue</th>
<th>user_count</th>
<th>churned_users</th>
<th>churn_rate</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
</tbody>
<tbody>
| Organic | 145.91 | 9 | 0 | 0.000000 |
| Paid | 174.94 | 6 | 1 | 16.666667 |
| Refferal | 74.94 | 6 | 5 | 83.333333 |
In [72]:
# 5.ARPU - avg revenue per user
df.columns
Out[72]:
Index(['customerid', 'subscription_start_date', 'subscription_type',
'renewal_date', 'plan_type', 'contract_type', 'cancellation_date',
'cancellation_reason', 'monthly_charges', 'cltv', 'churn_score',
'churn_flag', 'customer_name', 'country', 'state', 'gender', 'dob',
'complaint_date', 'escalations', 'csat_score', 'complaint_count'],
dtype='str')
In [73]:
arpu = df['monthly_charges'].mean()
print("ARPU =" , round(arpu,2))
ARPU = 18.85
In [74]:
#calculte customer age
today = pd.Timestamp.today()
df['age'] = (
today.year
- df['dob'].dt.year
- (
(today.month < df['dob'].dt.month)
| (
(today.month == df['dob'].dt.month)
& (today.day < df['dob'].dt.day)
)
)
)
df[['dob', 'age']]
Out[74]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>dob</th>
<th>age</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
<th>6</th>
<th>7</th>
<th>8</th>
<th>9</th>
<th>10</th>
<th>11</th>
<th>12</th>
<th>13</th>
<th>14</th>
<th>15</th>
<th>16</th>
<th>17</th>
<th>18</th>
<th>19</th>
<th>20</th>
</tbody>
<tbody>
| 1982-04-12 | 44 |
| 1995-11-23 | 30 |
| 1978-02-15 | 48 |
| 2001-08-30 | 24 |
| 1990-05-05 | 36 |
| 1988-12-10 | 37 |
| 1976-09-21 | 49 |
| 1999-03-14 | 27 |
| 1985-07-07 | 41 |
| 1993-10-29 | 32 |
| 1997-01-22 | 29 |
| 1981-06-18 | 45 |
| 2004-12-01 | 21 |
| 1992-04-25 | 34 |
| 1979-11-11 | 46 |
| 1986-02-28 | 40 |
| 1994-08-19 | 31 |
| 2000-09-02 | 25 |
| 1983-12-30 | 42 |
| 1991-05-14 | 35 |
| 1977-10-06 | 48 |
In [75]:
# 6. no. of days between the tenure (avg customer tenture)
# count of days user has used our service: cancellation date else current date
In [76]:
today = pd.Timestamp.today()
#print(today)
df['tenure_days'] = np.where(
df['cancellation_date'].notna(),
(df['cancellation_date'] - df['subscription_start_date']).dt.days,
(today - df['subscription_start_date']).dt.days
)
df
avg_tenture = df['tenure_days'].mean()
print("AVG TENTURE (Days) =",round(avg_tenture),0)
AVG TENTURE (Days) = 1518 0
In [77]:
# 7. revenue at risk - revenue lost from churned users
revenue_at_risk = df.loc[df['churn_flag'] == 1 , 'monthly_charges'].sum()
print("Revenue at Risk(Rs K) = " , revenue_at_risk)
Revenue at Risk(Rs K) = 73.94
In [78]:
# 8. escalation rate
escalation_rate = (df['escalations'] == 'Y').mean()*100
print("escalation rate is :",escalation_rate)
escalation rate is : 19.047619047619047
In [79]:
# 9. avg complaint rate per user
avg_complaint_rate = df['complaint_count'].mean()
print("Avg complaint rate is :",round(avg_complaint_rate,2))
Avg complaint rate is : 1.29
In [80]:
# 10 . correlation escalation vs churn
#df['escalations'] = np.where(df['escalations'] == 'Y',1,0) #encoding string to n type
corr_df = df[['escalations' , 'churn_flag' ]].dropna()
#correlation
correlation = corr_df['escalations'].corr(df['churn_flag'])
print("correlation is :",correlation)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[80], line 8 4 #df['escalations'] = np.where(df['escalations'] == 'Y',1,0) #encoding string to n type 5 6 corr_df = df[['escalations' , 'churn_flag' ]].dropna() 7 #correlation ----> 8 correlation = corr_df['escalations'].corr(df['churn_flag']) 9 print("correlation is :",correlation) File ~\AppData\Roaming\Python\Python314\site-packages\pandas\core\series.py:2770, in Series.corr(self, other, method, min_periods) 2767 if len(this) == 0: 2768 return np.nan -> 2770 this_values = this.to_numpy(dtype=float, na_value=np.nan, copy=False) 2771 other_values = other.to_numpy(dtype=float, na_value=np.nan, copy=False) 2773 if method in ["pearson", "spearman", "kendall"] or callable(method): File ~\AppData\Roaming\Python\Python314\site-packages\pandas\core\base.py:689, in IndexOpsMixin.to_numpy(self, dtype, copy, na_value, **kwargs) 685 values = values.copy() 687 values[np.asanyarray(isna(self))] = na_value --> 689 result = np.asarray(values, dtype=dtype) 691 if (copy and not fillna) or not copy: 692 if np.shares_memory(self._values[:2], result[:2]): 693 # Take slices to improve performance of check ValueError: could not convert string to float: 'Y'
In [ ]:
df.head(3)
In [81]:
# 11. churn risk create a column using existing columns
#df['churn_score'].head()
conditions = [
(df['churn_score'] < 50),
(df['churn_score'] >= 50) & (df['churn_score'] < 70),
(df['churn_score'] >= 70)
]
choices = ['low','mid','high']
df['churn_risk'] = np.select(conditions,choices,default='unknown')
In [82]:
df[['churn_risk','churn_score']].head()
Out[82]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>churn_risk</th>
<th>churn_score</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| low | 12 |
| high | 91 |
| low | 34 |
| low | 8 |
| high | 88 |
In [ ]:
In [ ]:
# 4. data visualization using matplotlib
In [88]:
df.columns
Out[88]:
Index(['customerid', 'subscription_start_date', 'subscription_type',
'renewal_date', 'plan_type', 'contract_type', 'cancellation_date',
'cancellation_reason', 'monthly_charges', 'cltv', 'churn_score',
'churn_flag', 'customer_name', 'country', 'state', 'gender', 'dob',
'complaint_date', 'escalations', 'csat_score', 'complaint_count', 'age',
'tenure_days', 'churn_risk'],
dtype='str')
In [83]:
df.head()
Out[83]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>customerid</th>
<th>subscription_start_date</th>
<th>subscription_type</th>
<th>renewal_date</th>
<th>plan_type</th>
<th>contract_type</th>
<th>cancellation_date</th>
<th>cancellation_reason</th>
<th>monthly_charges</th>
<th>cltv</th>
<th>...</th>
<th>state</th>
<th>gender</th>
<th>dob</th>
<th>complaint_date</th>
<th>escalations</th>
<th>csat_score</th>
<th>complaint_count</th>
<th>age</th>
<th>tenure_days</th>
<th>churn_risk</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
<p>5 rows × 24 columns</p>
| 0002-ORFBO | 2021-03-15 | Refferal | 2025-03-15 | Standard | Annual | NaT | NaN | 13.99 | 627 | ... | Maharashtra | Male | 1982-04-12 | NaT | NaN | NaN | NaN | 44 | 1980.0 | low |
| 0003-MKNFE | 2020-08-01 | Paid | 2024-08-01 | Premium | Annual | 2024-09-10 | Switched to competitor | 12.99 | 1150 | ... | Karnataka | Male | 1995-11-23 | 2024-08-28 | Y | 10.0 | 2.0 | 30 | 1501.0 | high |
| 0004-TLHLJ | 2022-11-20 | Organic | 2025-11-20 | Basic | Monthly | NaT | NaN | 6.99 | 210 | ... | Delhi | Female | 1978-02-15 | NaT | NaN | NaN | NaN | 48 | 1365.0 | low |
| 0011-IGKFF | 2019-05-10 | Paid | 2025-05-10 | Premium | Annual | NaT | NaN | 22.99 | 1725 | ... | Nagaland | Male | 2001-08-30 | NaT | NaN | NaN | NaN | 24 | 2655.0 | low |
| 0013-EXCHZ | 2023-01-05 | Refferal | 2024-01-05 | Standard | Monthly | 2024-02-28 | Too expensive | 13.99 | 195 | ... | Delhi | Female | 1990-05-05 | 2024-01-20 | Y | 20.0 | 1.0 | 36 | 419.0 | high |
In [84]:
df_visual = df.copy()
In [85]:
df_visual.shape
Out[85]:
(21, 24)
In [100]:
# 4.1 monthly churn trend (this is time series kpi)
df_visual['cancellation_month'] = df_visual['cancellation_date'].dt.to_period('M')
churn_trend = df_visual[df_visual['churn_flag'] == 1 ].groupby('cancellation_month').size()
plt.figure(figsize=(8,3))
plt.plot(
churn_trend.index.astype(str),
churn_trend.values,
color='green',
marker='o',
linestyle='dashed',
linewidth=2,
markersize=12
)
plt.title('Monthly churn trend')
plt.xlabel('Month')
plt.ylabel('churned customer')
plt.show()
In [104]:
# 4.2 churn by plan type
# Churned users by plan type
churn_by_plan = (
df.groupby('plan_type')['churn_flag']
.sum()
.sort_values(ascending=False)
)
churn_by_plan
plt.figure(figsize=(8, 5))
plt.bar(
churn_by_plan.index,
churn_by_plan.values
)
plt.title('Churn by Plan Type')
plt.xlabel('Plan Type')
plt.ylabel('Number of Churned Customers')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
In [105]:
#4.3 churn by state
import matplotlib.pyplot as plt
# Calculate churned customers by state
churn_by_state = (
df.groupby('state')['churn_flag']
.sum()
.sort_values(ascending=True)
)
# Plot
plt.figure(figsize=(10, 7))
plt.barh(
churn_by_state.index,
churn_by_state.values
)
plt.title('Churn by State')
plt.xlabel('Number of Churned Customers')
plt.ylabel('State')
plt.tight_layout()
plt.show()
In [ ]:
# 5. data visualization using seaborn
In [107]:
#encodig - convert str to numeric so that we can find corr between features
df_visual.columns
Out[107]:
Index(['customerid', 'subscription_start_date', 'subscription_type',
'renewal_date', 'plan_type', 'contract_type', 'cancellation_date',
'cancellation_reason', 'monthly_charges', 'cltv', 'churn_score',
'churn_flag', 'customer_name', 'country', 'state', 'gender', 'dob',
'complaint_date', 'escalations', 'csat_score', 'complaint_count', 'age',
'tenure_days', 'churn_risk', 'cancellation_month'],
dtype='str')
In [108]:
#df_encoded =
df_visual[['plan_type','contract_type','churn_score','churn_flag','churn_flag','escalations']].head()
Out[108]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>plan_type</th>
<th>contract_type</th>
<th>churn_score</th>
<th>churn_flag</th>
<th>churn_flag</th>
<th>escalations</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| Standard | Annual | 12 | 0 | 0 | NaN |
| Premium | Annual | 91 | 1 | 1 | Y |
| Basic | Monthly | 34 | 0 | 0 | NaN |
| Premium | Annual | 8 | 0 | 0 | NaN |
| Standard | Monthly | 88 | 1 | 1 | Y |
In [114]:
df_encoded.head()
Out[114]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>plan_type</th>
<th>contract_type</th>
<th>churn_score</th>
<th>churn_flag</th>
<th>escalations</th>
<th>churn_risk</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| 2 | 0 | 12 | 0 | -1 | 1 |
| 1 | 0 | 91 | 1 | 1 | 0 |
| 0 | 1 | 34 | 0 | -1 | 1 |
| 1 | 0 | 8 | 0 | -1 | 1 |
| 2 | 1 | 88 | 1 | 1 | 0 |
In [118]:
# Select required columns
# incorrect method of encoding as numbers are not assigned based on priority
df_encoded = df_visual[
['plan_type',
'contract_type',
'churn_score',
'churn_flag',
'escalations',
'churn_risk']
].copy()
# Categorical columns to encode
categorical_cols = [
'plan_type',
'contract_type',
'churn_risk',
'escalations'
]
# Convert categorical columns into numeric codes
for col in categorical_cols:
df_encoded[col] = df_encoded[col].astype('category').cat.codes
df_encoded.head()
Out[118]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>plan_type</th>
<th>contract_type</th>
<th>churn_score</th>
<th>churn_flag</th>
<th>escalations</th>
<th>churn_risk</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| 2 | 0 | 12 | 0 | -1 | 1 |
| 1 | 0 | 91 | 1 | 1 | 0 |
| 0 | 1 | 34 | 0 | -1 | 1 |
| 1 | 0 | 8 | 0 | -1 | 1 |
| 2 | 1 | 88 | 1 | 1 | 0 |
In [117]:
# Heatmap(correlation matrix)
import seaborn as sns
import matplotlib.pyplot as plt
sns.heatmap(
df_encoded.corr(),
annot=True
)
plt.show()
In [119]:
# Select required columns
# correct method of encoding based on priority
df_encoded = df_visual[
[
'plan_type',
'contract_type',
'churn_risk',
'churn_score',
'churn_flag',
'escalations'
]
].copy()
# Define priority/order manually
plan_mapping = {
'basic': 0,
'standard': 1,
'premium': 2
}
contract_mapping = {
'monthly': 0,
'annual': 1
}
risk_mapping = {
'low': 0,
'med': 1,
'high': 2
}
# Apply encoding
df_encoded['plan_type'] = (
df_encoded['plan_type']
.str.lower()
.str.strip()
.map(plan_mapping)
)
df_encoded['contract_type'] = (
df_encoded['contract_type']
.str.lower()
.str.strip()
.map(contract_mapping)
)
df_encoded['churn_risk'] = (
df_encoded['churn_risk']
.str.lower()
.str.strip()
.map(risk_mapping)
)
# Escalations Y/N
df_encoded['escalations'] = (
df_encoded['escalations']
.map({'N': 0, 'Y': 1})
)
df_encoded.head()
Out[119]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>plan_type</th>
<th>contract_type</th>
<th>churn_risk</th>
<th>churn_score</th>
<th>churn_flag</th>
<th>escalations</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| 1 | 1 | 0.0 | 12 | 0 | NaN |
| 2 | 1 | 2.0 | 91 | 1 | 1.0 |
| 0 | 0 | 0.0 | 34 | 0 | NaN |
| 2 | 1 | 0.0 | 8 | 0 | NaN |
| 1 | 0 | 2.0 | 88 | 1 | 1.0 |
In [120]:
# Heatmap(correlation matrix)
import seaborn as sns
import matplotlib.pyplot as plt
sns.heatmap(
df_encoded.corr(),
annot=True
)
plt.show()
In [121]:
import matplotlib.pyplot as plt
import numpy as np
# Correlation matrix
corr_matrix = df_encoded.corr()
# Create figure
plt.figure(figsize=(9, 7))
# Create heatmap
plt.imshow(corr_matrix, aspect='auto')
# Add colorbar
plt.colorbar(label='Correlation')
# Axis labels
plt.xticks(
range(len(corr_matrix.columns)),
corr_matrix.columns,
rotation=45,
ha='right'
)
plt.yticks(
range(len(corr_matrix.columns)),
corr_matrix.columns
)
# Add correlation values inside each cell
for i in range(len(corr_matrix.columns)):
for j in range(len(corr_matrix.columns)):
plt.text(
j,
i,
f'{corr_matrix.iloc[i, j]:.2f}',
ha='center',
va='center'
)
plt.title('Correlation Heatmap')
plt.tight_layout()
plt.show()
In [122]:
# pair plot for relationship between our dataset
sns.pairplot(df_encoded)
plt.show()
In [126]:
# catplot/ facegrid plot - multi dimension comaprision
sns.catplot(data=df_visual,
x='plan_type',
y='monthly_charges',
hue='gender',
col='churn_risk')
Out[126]:
<seaborn.axisgrid.FacetGrid at 0x29f6401a120>
In [ ]:
In [ ]:
# pivot tabel using pandas
In [127]:
pd.pivot_table(
df_visual,
index = 'plan_type',
values = 'churn_flag',
aggfunc = 'mean',
)
Out[127]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>churn_flag</th>
<th>plan_type</th>
<th></th>
</thead>
<tbody>
<th>Basic</th>
<th>Premium</th>
<th>Standard</th>
</tbody>
<tbody>
| 0.600000 |
| 0.142857 |
| 0.222222 |
In [ ]:
In [128]:
pd.pivot_table(
df_visual,
index = 'plan_type',
values = ['monthly_charges','customerid','churn_flag'],
aggfunc = {'monthly_charges': 'sum',
'customerid' : 'nunique',
'churn_flag':'mean',
}
)
Out[128]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>churn_flag</th>
<th>customerid</th>
<th>monthly_charges</th>
<th>plan_type</th>
<th></th>
<th></th>
<th></th>
</thead>
<tbody>
<th>Basic</th>
<th>Premium</th>
<th>Standard</th>
</tbody>
<tbody>
| 0.600000 | 5 | 52.95 |
| 0.142857 | 7 | 218.93 |
| 0.222222 | 9 | 123.91 |
In [ ]:
# working with sql in python (pandas)
In [129]:
#create db in sql
conn = sqlite3.connect('test_database.sqlite')
#table details
conn.execute("CREATE TABLE users (first_name TEXT, country TEXT, budget INTEGER)")
#commit and save
conn.commit()
In [131]:
# insert data
cursor = conn.cursor()
cursor.execute("""
INSERT INTO users VALUES
('Aarav', 'India', 6200),
('Emma', 'USA', 7500),
('Lucas', 'Germany', 6800),
('Sophia', 'Canada', 7100)
"""
)
#commit and save
conn.commit()
print("data inserted successfully")
data inserted successfully
In [132]:
#check inserted data in table
conn = sqlite3.connect('test_database.sqlite')
query = """SELECT * FROM users"""
df_results = pd.read_sql(query,conn)
df_results.head()
Out[132]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>first_name</th>
<th>country</th>
<th>budget</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
</tbody>
<tbody>
| Aarav | India | 6200 |
| Emma | USA | 7500 |
| Lucas | Germany | 6800 |
| Sophia | Canada | 7100 |
| Aarav | India | 6200 |
In [133]:
#aggregation
query = """
SELECT country, sum(budget) as total_budget
FROM users
GROUP BY country
"""
df_agg = pd.read_sql(query,conn)
df_agg
Out[133]:
<style scoped>
.dataframe tbody tr th:only-of-type {
vertical-align: middle;
}
.dataframe tbody tr th {
vertical-align: top;
}
.dataframe thead th {
text-align: right;
}
</style>
<thead>
<th></th>
<th>country</th>
<th>total_budget</th>
</thead>
<tbody>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
</tbody>
<tbody>
| Canada | 14200 |
| Germany | 13600 |
| India | 12400 |
| USA | 15000 |
In [135]:
# ==========================================
# KEY INSIGHTS FROM CUSTOMER CHURN ANALYSIS
# ==========================================
# 1. Total customers
total_customers = df['customerid'].nunique()
# 2. Total churned customers
total_churned = df['churn_flag'].sum()
# 3. Overall churn rate
churn_rate = df['churn_flag'].mean() * 100
# 4. Average customer age
avg_age = df['age'].mean()
# 5. Average customer tenure
avg_tenure = df['tenure_days'].mean()
# 6. Average monthly charges
avg_monthly_charges = df['monthly_charges'].mean()
# 7. Total Revenue
total_revenue = df['monthly_charges'].sum()
# 8. Revenue loss due to churn
revenue_loss_churn = df.loc[
df['churn_flag'] == 1,
'monthly_charges'
].sum()
# 9. Revenue loss percentage
revenue_loss_percentage = (
revenue_loss_churn / total_revenue
) * 100
# 10. Average CLTV
avg_cltv = df['cltv'].mean()
# 11. Average complaints per customer
avg_complaints = df['complaint_count'].mean()
# 12. Escalation rate
escalation_rate = (
df['escalations'] == 'Y'
).mean() * 100
# 13. Plan type churn rate
plan_churn = (
df.groupby('plan_type')['churn_flag']
.mean()
.mul(100)
.sort_values(ascending=False)
)
highest_churn_plan = plan_churn.index[0]
highest_churn_plan_rate = plan_churn.iloc[0]
# 14. Subscription type churn rate
subscription_churn = (
df.groupby('subscription_type')['churn_flag']
.mean()
.mul(100)
.sort_values(ascending=False)
)
highest_churn_subscription = subscription_churn.index[0]
highest_churn_subscription_rate = subscription_churn.iloc[0]
# 15. State churn rate
state_churn = (
df.groupby('state')['churn_flag']
.mean()
.mul(100)
.sort_values(ascending=False)
)
highest_churn_state = state_churn.index[0]
highest_churn_state_rate = state_churn.iloc[0]
# 16. Monthly vs Annual contract churn percentage
contract_churn = (
df.groupby('contract_type')['churn_flag']
.mean()
.mul(100)
)
monthly_churn = contract_churn.get('monthly', 0)
annual_churn = contract_churn.get('annual', 0)
# ==================
# PRINT THE INSIGHTS
# ==================
print("CUSTOMER CHURN - KEY INSIGHTS")
print("=" * 60)
print(f"1. Total Customers: {total_customers:,}")
print(f"2. Total Churned Customers: {total_churned:,}")
print(f"3. Overall Churn Rate: {churn_rate:.2f}%")
print(f"4. Average Customer Age: {avg_age:.1f} years")
print(f"5. Average Customer Tenure: {avg_tenure:.1f} days")
print(f"6. Average Monthly Charges: {avg_monthly_charges:,.2f}")
print(f"7. Total Revenue: {total_revenue:,.2f}")
print(
f"8. Revenue Loss Due to Churn: "
f"{revenue_loss_churn:,.2f}"
)
print(
f"9. Revenue Lost Due to Churn (%): "
f"{revenue_loss_percentage:.2f}%"
)
print(f"10. Average CLTV: {avg_cltv:,.2f}")
print(
f"11. Average Complaints per Customer: "
f"{avg_complaints:.2f}"
)
print(f"12. Escalation Rate: {escalation_rate:.2f}%")
print(
f"13. Highest Churn Plan: "
f"{highest_churn_plan} "
f"({highest_churn_plan_rate:.2f}%)"
)
print(
f"14. Highest Churn Subscription Type: "
f"{highest_churn_subscription} "
f"({highest_churn_subscription_rate:.2f}%)"
)
print(
f"15. State with Highest Churn Rate: "
f"{highest_churn_state} "
f"({highest_churn_state_rate:.2f}%)"
)
print("\nCONTRACT TYPE CHURN")
print("-" * 60)
print(f"16. Monthly Contract Churn Rate: {monthly_churn:.2f}%")
print(f"17. Annual Contract Churn Rate: {annual_churn:.2f}%")
CUSTOMER CHURN - KEY INSIGHTS ============================================================ 1. Total Customers: 21 2. Total Churned Customers: 6 3. Overall Churn Rate: 28.57% 4. Average Customer Age: 36.4 years 5. Average Customer Tenure: 1518.0 days 6. Average Monthly Charges: 18.85 7. Total Revenue: 395.79 8. Revenue Loss Due to Churn: 73.94 9. Revenue Lost Due to Churn (%): 18.68% 10. Average CLTV: 823.52 11. Average Complaints per Customer: 1.29 12. Escalation Rate: 19.05% 13. Highest Churn Plan: Basic (60.00%) 14. Highest Churn Subscription Type: Refferal (83.33%) 15. State with Highest Churn Rate: Karnataka (100.00%) CONTRACT TYPE CHURN ------------------------------------------------------------ 16. Monthly Contract Churn Rate: 0.00% 17. Annual Contract Churn Rate: 0.00%
In [ ]:
In [ ]:
In [ ]: