Skip to content

Management, finance and ESGBI, AI and ML for the business

B2B customers at risk of churning

A customer who is leaving does not announce it: they order less often and spend less. Every Monday Muvia scores each customer's risk with a Python function on their orders and sends every sales rep the list of who to call first, ranked by value.

Illustrative scenario: it describes a typical case, not a customer project.

By the time the drop reaches revenue, the customer has gone.

In B2B a customer rarely ends the relationship with a phone call. They order every six weeks instead of every three, buy fewer lines, move part of their purchases elsewhere. Monthly totals hide it, and the sales rep notices when it is too late.

The signal is already in the orders: it has to be worked out for each customer, compared with that customer's own history and put in front of the person who can pick up the phone, every week.

Who it's for
Sales leadership, area managers and sales reps; customer service and management accounting for the value at risk.
Orders from the ERP on SQL Server, customers and sales reps from Salesforce and sales channels from an Excel file flow into Muvia, where a Python function scores every customer's risk each Monday; out come a score table, a dashboard by rep, the call list as a PDF and an alarm.

In Muvia, step by step

Real product screens, recorded on a project with sample data.

1 of 5

The video · A customer who is leaving does not announce it: they order less often and spend less. Every Monday Muvia scores each customer's risk with a Python function on their orders and sends every sales rep the list of who to call first, ranked by value.

What you get

The drop shows in the orders
Risk rises as the customer slows down, weeks before the quarter's revenue shows it.
Every rep knows who to call
The list arrives on Monday morning, already ranked by value, without asking for an extract.
Risk has a value
Next to the score sits the customer's revenue: management sees what is at stake and where it is concentrated.
The rule belongs to the team
The score is code the team can read and change; a new rule is a new version of the function, not another spreadsheet.

For the technical team

How it is built in Muvia

  1. 1

    Connect orders and the CRM

    Orders arrive with an incremental copy of the ERP database every night, customers and reps through the Salesforce connector. A query joins them on the customer code.

  2. 2

    Write the risk in Python

    A Python function compares each customer's order frequency and spend over the last three months with their past year and turns it into a score. A trial run on a sample writes nothing; Athena can draft the code.

  3. 3

    Schedule it every Monday

    A flow runs the function every Monday at 7 am and writes the scores to a managed table, which dashboards, notebooks and alarms read like any other data.

  4. 4

    Show risk by sales rep

    A dashboard plots each customer's risk against their value and adds up the revenue at risk per rep: you see at once where it is concentrated.

  5. 5

    Send the list to the rep

    A notebook with the customers to call, ranked by value, becomes a PDF that arrives by email on Monday morning. An alarm flags a customer entering the high band.

Python function

import numpy as np
import pandas as pd

def transform(inputs, params, ctx):
    df = inputs["rows"]
    df["order_date"] = pd.to_datetime(df["order_date"])
    recent = df["order_date"] > df["order_date"].max() - pd.Timedelta(days=90)
    share = 90 / 365
    year = df.groupby(["customer_code", "sales_rep"], as_index=False).agg(
        orders_year=("order_number", "nunique"), spend_year=("net_amount", "sum"))
    last_90 = df[recent].groupby("customer_code", as_index=False).agg(
        orders_90d=("order_number", "nunique"), spend_90d=("net_amount", "sum"))
    out = year.merge(last_90, on="customer_code", how="left").fillna(0)
    expected = (out["orders_year"] * share).replace(0, np.nan)
    out["frequency_drop"] = (1 - out["orders_90d"] / expected).clip(0, 1)
    expected_spend = (out["spend_year"] * share).replace(0, np.nan)
    out["spend_drop"] = (1 - out["spend_90d"] / expected_spend).clip(0, 1)
    weight = params["frequency_weight"]
    out["risk"] = (weight * out["frequency_drop"]
                   + (1 - weight) * out["spend_drop"]).fillna(0).round(2)
    out["band"] = np.where(out["risk"] >= params["threshold"], "high", "normal")
    return {"rows": out}
Order frequency and spend over the last three months against each customer's past year: the score weighs the two drops and the threshold sets the band.
The data it needs
  • Orders and order lines per customer over the last twelve months, from the ERP on SQL Server
  • Customers, assigned sales rep and region from Salesforce
  • Channel (wholesale or large retail) and terms, from an Excel file
Parts of Muvia used

Bring us a question you can't answer today.

We start from a real question your business has and walk the path from source to dashboard with your systems, not a demo dataset.