Data Cleaning — Customer Churn Analysis¶
Setup¶
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
df = pd.read_csv("WA_Fn-UseC_-Telco-Customer-Churn.csv")
print(f"Shape: {df.shape}")
print(f"\nMissing values:\n{df.isnull().sum()[df.isnull().sum() > 0]}")
print(f"\nDuplicate rows: {df.duplicated().sum()}")
Step 1 — Fix TotalCharges Type¶
TotalCharges is stored as a string and has blank values for new customers.
# Inspect the problem
print(df["TotalCharges"].dtype) # object
print(df[df["TotalCharges"] == " "]) # 11 rows with spaces
# Fix: convert to numeric, blanks become NaN
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
# For customers with tenure=0, TotalCharges should be 0
df.loc[(df["tenure"] == 0) & df["TotalCharges"].isna(), "TotalCharges"] = 0
print(f"Remaining NaN in TotalCharges: {df['TotalCharges'].isna().sum()}")
Step 2 — Standardise SeniorCitizen¶
SeniorCitizen is 0/1 while all other binary fields are Yes/No — inconsistent.
df["SeniorCitizen"] = df["SeniorCitizen"].map({0: "No", 1: "Yes"})
print(df["SeniorCitizen"].value_counts())
Step 3 — Simplify Internet Add-On Columns¶
Five columns use "No internet service" as a third value — effectively the same as "No" for our analysis.
internet_cols = ["OnlineSecurity", "OnlineBackup", "DeviceProtection",
"TechSupport", "StreamingTV", "StreamingMovies", "MultipleLines"]
for col in internet_cols:
df[col] = df[col].replace("No internet service", "No")
df[col] = df[col].replace("No phone service", "No")
print(df[internet_cols].apply(lambda x: x.unique()))
Step 4 — Convert Target to Binary¶
df["Churn"] = df["Churn"].map({"Yes": 1, "No": 0})
print(f"Churn rate: {df['Churn'].mean():.1%}") # 26.5%
Step 5 — Feature Engineering¶
Create derived features that will improve our analysis.
# Tenure buckets
df["tenure_bucket"] = pd.cut(
df["tenure"],
bins=[-1, 6, 24, 48, 72],
labels=["New (< 6mo)", "Growing (6-24mo)", "Established (2-4yr)", "Loyal (4+ yr)"]
)
# Add-on count — how many services does the customer have?
addons = ["OnlineSecurity", "OnlineBackup", "DeviceProtection",
"TechSupport", "StreamingTV", "StreamingMovies"]
df["addon_count"] = (df[addons] == "Yes").sum(axis=1)
# Revenue per month per add-on (proxy for engagement)
df["monthly_charge_per_addon"] = df["MonthlyCharges"] / (df["addon_count"] + 1)
# Has automatic payment?
df["auto_payment"] = df["PaymentMethod"].isin(
["Bank transfer (automatic)", "Credit card (automatic)"]
).astype(int)
# Is a multi-year contract?
df["long_term_contract"] = (df["Contract"] != "Month-to-month").astype(int)
Step 6 — Audit After Cleaning¶
def audit(df, label=""):
print(f"\n{'='*50}")
print(f"AUDIT: {label}")
print(f"Shape: {df.shape}")
print(f"Missing: {df.isnull().sum().sum()} cells")
print(f"Duplicates: {df.duplicated().sum()}")
print(f"Churn rate: {df['Churn'].mean():.1%}")
print(f"Avg tenure: {df['tenure'].mean():.1f} months")
print(f"Avg monthly charge: £{df['MonthlyCharges'].mean():.2f}")
audit(df, "After Cleaning")
# Save clean dataset
df.to_csv("churn_clean.csv", index=False)
print("\nSaved to churn_clean.csv")
Distribution Checks¶
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
# Tenure by churn status
df.groupby("Churn")["tenure"].hist(ax=axes[0, 0], alpha=0.7, bins=24)
axes[0, 0].set_title("Tenure Distribution by Churn Status")
axes[0, 0].legend(["Not Churned (0)", "Churned (1)"])
# Monthly charges by churn
df.boxplot(column="MonthlyCharges", by="Churn", ax=axes[0, 1])
axes[0, 1].set_title("Monthly Charges by Churn Status")
# Contract type distribution
df["Contract"].value_counts().plot(kind="bar", ax=axes[0, 2], color="#0D9488")
axes[0, 2].set_title("Contract Type Distribution")
# Add-on count vs churn
df.groupby("addon_count")["Churn"].mean().plot(kind="bar", ax=axes[1, 0], color="#F59E0B")
axes[1, 0].set_title("Churn Rate by Add-on Count")
axes[1, 0].set_ylabel("Churn Rate")
# Tenure bucket churn rate
churn_by_tenure = df.groupby("tenure_bucket", observed=True)["Churn"].mean()
churn_by_tenure.plot(kind="bar", ax=axes[1, 1], color="#EF4444")
axes[1, 1].set_title("Churn Rate by Tenure Bucket")
# Internet service churn
churn_by_internet = df.groupby("InternetService")["Churn"].mean()
churn_by_internet.plot(kind="bar", ax=axes[1, 2], color="#0D9488")
axes[1, 2].set_title("Churn Rate by Internet Service")
plt.suptitle("Churn Analysis — Data Distribution", fontsize=14)
plt.tight_layout()
plt.savefig("churn_distributions.png", dpi=150, bbox_inches="tight")
plt.show()