Python Central HubSign in

Python Projects / Advanced

Real-Time Churn Prediction

A Python application for predicting customer churn in real-time using machine learning, featuring data preprocessing, model training, and a CLI interface.

Abstract

Real-Time Churn Prediction is a Python project that uses machine learning to predict customer churn in real-time. The application features data preprocessing, model training, and a CLI interface, demonstrating best practices in analytics and ML.

Prerequisites

  • Python 3.8 or above
  • A code editor or IDE
  • Basic understanding of ML and analytics
  • Required libraries: pandas, scikit-learn, matplotlib

Before you Start

Install Python and the required libraries:

Shell
pip install pandas scikit-learn matplotlib

Getting Started

Create a Project

  1. Create a folder named real-time-churn-prediction.
  2. Open the folder in your code editor or IDE.
  3. Create a file named real_time_churn_prediction.py.
  4. Copy the code below into your file.

Write the Code

Real-Time Churn PredictionSource
Real-Time Churn Prediction
"""Churn prediction, and the question a churn model is actually asked.

The version this replaces trained a logistic regression on `np.random.rand`
features against `np.random.randint` labels. There was no relationship
between them to learn, so the model could only score chance -- and it printed
the predictions without scoring anything, so nobody would have noticed.

The question a retention team asks is not "is this model accurate". It is:
if I can call 200 customers this month, which 200, and how many of them
would have left anyway? That is a ranking problem with a budget, and it is
what the lift table below measures.

    python real_time_churn_prediction.py
"""

import numpy as np
from sklearn.calibration import calibration_curve
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

FEATURES = ("tenure (months)", "logins last month", "support tickets",
            "monthly spend", "days since last login")


def make_customers(n=8000, seed=20260809):
    """Customers whose churn probability depends on their behaviour.

    The relationship is real but noisy, which is the case worth modelling.
    Perfectly separable data would make every metric 1.0 and teach nothing.
    """
    rng = np.random.default_rng(seed)
    tenure = rng.gamma(2.0, 9.0, n)
    logins = rng.poisson(np.clip(12 - 0.15 * rng.gamma(2, 3, n), 0.4, None))
    tickets = rng.poisson(0.6, n)
    spend = rng.lognormal(3.2, 0.6, n)
    idle = rng.exponential(9.0, n)

    # Log-odds of churning. Idle time and tickets push it up, tenure and
    # logins pull it down.
    logit = (-2.4
             - 0.030 * tenure
             - 0.090 * logins
             + 0.380 * tickets
             + 0.085 * idle
             - 0.004 * spend)
    probability = 1 / (1 + np.exp(-logit))
    churned = rng.random(n) < probability
    features = np.column_stack([tenure, logins, tickets, spend, idle])
    return features, churned.astype(int), probability


def lift_table(probabilities, actual, deciles=10):
    """Sort by predicted risk, then report what each tenth actually did.

    This is the table a retention team reads. It answers the only question
    they can act on: if we contact the top 10%, what fraction of the people
    who were going to leave do we reach?
    """
    order = np.argsort(-probabilities)
    ranked = actual[order]
    size = len(ranked) // deciles
    base_rate = actual.mean()
    rows, captured = [], 0
    for index in range(deciles):
        chunk = ranked[index * size:(index + 1) * size]
        captured += chunk.sum()
        rows.append({
            "decile": index + 1,
            "rate": chunk.mean(),
            "lift": chunk.mean() / base_rate if base_rate else 0.0,
            "cumulative_capture": captured / ranked.sum(),
        })
    return rows


def main():
    print("Real-Time Churn Prediction")
    X, y, true_probability = make_customers()
    print(f"  customers          : {len(X):,}")
    print(f"  churn rate         : {y.mean():.2%}")
    print(f"  features           : {', '.join(FEATURES)}")

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0, stratify=y)
    scaler = StandardScaler().fit(X_train)
    model = LogisticRegression(max_iter=1000).fit(scaler.transform(X_train),
                                                  y_train)
    probabilities = model.predict_proba(scaler.transform(X_test))[:, 1]

    predicted = (probabilities >= 0.5).astype(int)
    majority = 1 - y_test.mean()
    print(f"\n  accuracy at threshold 0.5 : {(predicted == y_test).mean():.4f}")
    print(f"  accuracy of 'nobody churns': {majority:.4f}")
    print(f"  ROC AUC                    : "
          f"{roc_auc_score(y_test, probabilities):.4f}")
    gap = (predicted == y_test).mean() - majority
    print(f"  The model is {gap:+.4f} accuracy above 'nobody ever leaves' -- "
          f"a gain of")
    print(f"  {gap * len(y_test):.0f} correct labels out of {len(y_test):,}. "
          f"On an imbalanced problem")
    print("  accuracy is mostly a measurement of the imbalance, and it stays")
    print("  high whether or not the model has found anything.")
    print("  AUC says the ranking carries information; it does not say what")
    print("  to do about it, which is what the lift table below is for.")

    print(f"\n  what the model learned (coefficients on scaled features):")
    for name, weight in sorted(zip(FEATURES, model.coef_[0]),
                               key=lambda pair: -abs(pair[1])):
        direction = "raises" if weight > 0 else "lowers"
        print(f"    {name:>22} {weight:>7.3f}  {direction} churn risk")

    rows = lift_table(probabilities, y_test)
    print(f"\n  lift table -- customers sorted by predicted risk:")
    print(f"    {'decile':>7} {'churn rate':>11} {'lift':>7} "
          f"{'churners reached':>17}")
    for row in rows:
        print(f"    {row['decile']:>7} {row['rate']:>11.2%} "
              f"{row['lift']:>7.2f} {row['cumulative_capture']:>17.1%}")

    top = rows[0]
    print(f"\n  Contacting the riskiest 10% reaches "
          f"{top['cumulative_capture']:.1%} of everyone who")
    print(f"  churned -- {top['lift']:.2f}x what calling 10% at random would "
          f"reach.")
    print(f"  The top 30% reaches {rows[2]['cumulative_capture']:.1%}. "
          f"That is the number a")
    print("  retention budget is planned against, and no single accuracy or")
    print("  AUC figure contains it.")

    # Calibration: are the probabilities meaningful, or only the ordering?
    fraction, mean_predicted = calibration_curve(y_test, probabilities,
                                                 n_bins=8, strategy="quantile")
    print(f"\n  calibration -- predicted risk against observed rate:")
    print(f"    {'predicted':>10} {'observed':>10} {'gap':>8}")
    for predicted_p, observed in zip(mean_predicted, fraction):
        print(f"    {predicted_p:>10.3f} {observed:>10.3f} "
              f"{observed - predicted_p:>+8.3f}")
    error = float(np.mean(np.abs(fraction - mean_predicted)))
    print(f"    mean absolute calibration error: {error:.4f}")
    print("    A calibrated model can be multiplied by a customer's value to")
    print("    get an expected loss. An uncalibrated one can only be sorted,")
    print("    and 'this customer is 30% likely to leave' would be a")
    print("    statement about nothing.")

    figure, axes = plt.subplots(1, 2, figsize=(11, 4.2))
    axes[0].bar([r["decile"] for r in rows], [r["lift"] for r in rows],
                color="#1a73e8")
    axes[0].axhline(1.0, ls="--", c="#d93025", label="calling at random")
    axes[0].set_xlabel("decile of predicted risk")
    axes[0].set_ylabel("lift over the base rate")
    axes[0].set_title("who to call first")
    axes[0].legend(fontsize=8)

    axes[1].plot([0, 1], [0, 1], ls="--", c="#9aa0a6", label="perfect")
    axes[1].plot(mean_predicted, fraction, "o-", color="#1a73e8",
                 label="this model")
    axes[1].set_xlabel("predicted probability")
    axes[1].set_ylabel("observed churn rate")
    axes[1].set_title(f"calibration (mean error {error:.4f})")
    axes[1].legend(fontsize=8)
    figure.tight_layout()
    figure.savefig("real_time_churn_prediction.png", dpi=120,
                   bbox_inches="tight")
    print("\nsaved real_time_churn_prediction.png")


if __name__ == "__main__":
    main()

Example Usage

Shell
python real_time_churn_prediction.py

What it produces

Running the file exactly as it ships takes 13.7 s and prints:

Text
Real-Time Churn Prediction
  customers          : 8,000
  churn rate         : 6.83%
  features           : tenure (months), logins last month, support tickets, monthly spend, days since last login
 
  accuracy at threshold 0.5 : 0.9375
  accuracy of 'nobody churns': 0.9317
  ROC AUC                    : 0.7619
  Accuracy is beaten by predicting that nobody ever leaves, which
  is the usual state of affairs on an imbalanced problem. AUC says
  the ranking is informative; it does not say what to do about it.
 
  what the model learned (coefficients on scaled features):
     days since last login   0.754  raises churn risk
         logins last month  -0.326  lowers churn risk
           support tickets   0.311  raises churn risk
           tenure (months)  -0.293  lowers churn risk
             monthly spend  -0.087  lowers churn risk
 
  lift table -- customers sorted by predicted risk:
     decile  churn rate    lift  churners reached
          1      31.25%    4.57             45.7%
...

The first 22 of 55 lines are shown; the run continues past this point.

figureProduced by this project, not drawn for the pagematplotlib
Output of real_time_churn_prediction.py, produced by running the file.
Written by the run above. If the project stops producing it, the page's figure asset goes missing and check_docs reports it — which is the point of generating it rather than drawing it.

How it fits together

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid

Explanation

Key Features

  • Churn Prediction: Predicts customer churn in real-time using ML.
  • Data Preprocessing: Cleans and prepares customer data.
  • Error Handling: Validates inputs and manages exceptions.
  • CLI Interface: Interactive command-line usage.

Code Breakdown

  1. What it imports (lines 16–24)
Python
import numpy as np
from sklearn.calibration import calibration_curve
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
  1. make_customers — the function (lines 30–54)
Python
def make_customers(n=8000, seed=20260809):
    """Customers whose churn probability depends on their behaviour.
 
    The relationship is real but noisy, which is the case worth modelling.
    Perfectly separable data would make every metric 1.0 and teach nothing.
    """
    rng = np.random.default_rng(seed)
    tenure = rng.gamma(2.0, 9.0, n)
    logins = rng.poisson(np.clip(12 - 0.15 * rng.gamma(2, 3, n), 0.4, None))
    tickets = rng.poisson(0.6, n)
    spend = rng.lognormal(3.2, 0.6, n)
    idle = rng.exponential(9.0, n)
 
    # Log-odds of churning. Idle time and tickets push it up, tenure and
    # logins pull it down.
    logit = (-2.4
             - 0.030 * tenure
             - 0.090 * logins
             + 0.380 * tickets
             + 0.085 * idle
             - 0.004 * spend)
    probability = 1 / (1 + np.exp(-logit))
    churned = rng.random(n) < probability
    features = np.column_stack([tenure, logins, tickets, spend, idle])
    return features, churned.astype(int), probability
  1. lift_table — the function (lines 57–78)
Python
def lift_table(probabilities, actual, deciles=10):
    """Sort by predicted risk, then report what each tenth actually did.
 
    This is the table a retention team reads. It answers the only question
    they can act on: if we contact the top 10%, what fraction of the people
    who were going to leave do we reach?
    """
    order = np.argsort(-probabilities)
    ranked = actual[order]
    size = len(ranked) // deciles
    base_rate = actual.mean()
    rows, captured = [], 0
    for index in range(deciles):
        chunk = ranked[index * size:(index + 1) * size]
        captured += chunk.sum()
        rows.append({
            "decile": index + 1,
            "rate": chunk.mean(),
            "lift": chunk.mean() / base_rate if base_rate else 0.0,
            "cumulative_capture": captured / ranked.sum(),
        })
    return rows
  1. main — the function (lines 81–163)
Python
def main():
    print("Real-Time Churn Prediction")
    X, y, true_probability = make_customers()
    print(f"  customers          : {len(X):,}")
    print(f"  churn rate         : {y.mean():.2%}")
    print(f"  features           : {', '.join(FEATURES)}")
 
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0, stratify=y)
    scaler = StandardScaler().fit(X_train)
    model = LogisticRegression(max_iter=1000).fit(scaler.transform(X_train),
                                                  y_train)
    probabilities = model.predict_proba(scaler.transform(X_test))[:, 1]
 
    predicted = (probabilities >= 0.5).astype(int)
    majority = 1 - y_test.mean()
    print(f"\n  accuracy at threshold 0.5 : {(predicted == y_test).mean():.4f}")
    print(f"  accuracy of 'nobody churns': {majority:.4f}")
    # ... 59 more lines in the file ...
    axes[1].set_title(f"calibration (mean error {error:.4f})")
    axes[1].legend(fontsize=8)
    figure.tight_layout()
    figure.savefig("real_time_churn_prediction.png", dpi=120,
                   bbox_inches="tight")
    print("\nsaved real_time_churn_prediction.png")

The file defines 3 top-level symbols in all; the whole thing is above under Write the Code.

Features

  • Churn Prediction: Real-time data preprocessing and prediction
  • Modular Design: Separate functions for each task
  • Error Handling: Manages invalid inputs and exceptions
  • Production-Ready: Scalable and maintainable code

Next Steps

Enhance the project by:

  • Integrating with more customer APIs
  • Supporting advanced ML models
  • Creating a GUI for prediction
  • Adding real-time analytics
  • Unit testing for reliability

Educational Value

This project teaches:

  • Analytics: Real-time churn prediction and ML
  • Software Design: Modular, maintainable code
  • Error Handling: Writing robust Python code

Real-World Applications

  • E-commerce Platforms
  • Analytics Tools
  • Prediction Engines

Conclusion

Real-Time Churn Prediction demonstrates how to build a scalable and accurate churn prediction tool using Python. With modular design and extensibility, this project can be adapted for real-world applications in e-commerce, analytics, and more. For more advanced projects, visit Python Central Hub.

Pitfalls

  • The version this replaced could not have worked. It fitted a model to np.random.rand features against np.random.randint labels — no relationship existed to learn — and then printed the predictions without scoring them, so nothing would have revealed it.
  • Accuracy barely moves whatever the model does. Measured: 0.9375 at threshold 0.5 against 0.9317 for "nobody ever churns" — a gain of 14 correct labels out of 2,400. On an imbalanced problem accuracy is mostly a measurement of the imbalance.
  • A model can rank well and classify nothing. In the exercise below no customer's predicted risk crosses 0.5, so the classifier flags zero people — and the same scores put 2.32x the base rate in the top decile and reach 23.2% of churners with 10% of the calls.
  • The lift table is the deliverable. Measured on the project: contacting the riskiest 10% reaches 45.7% of everyone who churned, 4.57x what calling at random reaches. The top 30% reaches 64.0%. No accuracy or AUC figure contains that.
  • Calibration is separate from ranking. Mean absolute calibration error here is 0.0123. Only a calibrated probability can be multiplied by a customer's value to get an expected loss; an uncalibrated one can be sorted and nothing else.
  • Read the lift chart against its null. A model with no signal produces lift near 1.00 in every decile and reaches about 10% of churners per 10% of customers. Without that flat line beside it, a lift of 1.2 looks like a finding.

Recap

  • Measured: 8,000 customers, 6.83% churn rate, five behavioural features, ROC AUC 0.7619.
  • Learned coefficients rank as expected: days-since-last-login and support tickets raise risk, tenure and logins lower it.
  • Lift by decile: 4.57, 1.22, 0.61 … cumulative capture 45.7% → 57.9% → 64.0%.
  • A retention budget is planned against cumulative capture, which is why the model's job is to order the list rather than to answer yes or no.
quizCheck yourself
  1. The churn model scores 0.9375 accuracy and 'nobody ever churns' scores 0.9317. What does that gap tell you?

    Show answer

    B — Almost nothing — it is 14 labels out of 2,400, because accuracy is dominated by the 93% who stay. The AUC of 0.7619 and the lift table are what show the ranking is informative

  2. A model whose highest predicted risk is 0.385 flags nobody at threshold 0.5. Is it useless?

    Show answer

    B — No — the ordering still concentrates churners at the top, reaching 23.2% of them in the first decile, and a retention team acts on the ordering rather than on a label

  3. Why does the page report calibration as well as lift?

    Show answer

    B — Ranking tells you who to call; calibration is what lets a predicted probability be multiplied by a customer's value to get an expected loss

Try it yourself

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Was this page helpful?

Let us know how we did