Skip to content

Retail and consumer goodsBI, AI and ML for the business

Campaigns measured against the baseline

During a campaign sales go up, but part of that would have come anyway. In Muvia a Python function compares orders and revenue hour by hour with a typical week without campaigns, and every channel shows its net effect and when it pays back its spend.

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

Sales rise during the campaign. How much would they have risen anyway?

Every ad platform reports its own conversions, and adding them up gives more than revenue. Comparing with the week before does not help: there is the season, the August holidays, two campaigns overlapping. So the budget is split out of habit.

To know what a campaign brings you need a baseline: how much the store would have sold in those same hours without campaigns. Only the difference, net of spend and returns, says whether a channel pays back.

Who it's for
Marketing and sales management, e-commerce managers, management accounting.
Campaign spend from a REST API, online store orders and revenue from the database on PostgreSQL and the campaign calendar from Google Sheets flow into Muvia, where a Python function computes the orders expected without campaigns every morning; out come the net-effect table, an analysis of campaigns aligned on launch, each campaign's balance and Athena's answers.

In Muvia, step by step

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

1 of 5

The video · During a campaign sales go up, but part of that would have come anyway. In Muvia a Python function compares orders and revenue hour by hour with a typical week without campaigns, and every channel shows its net effect and when it pays back its spend.

What you get

Net effect, not reported conversions
Every campaign is measured against what the store would have sold anyway.
Channels you can compare
Email, social, search and TV sit on the same scale, aligned on the moment of launch.
You know when spend pays back
The hour-by-hour balance says whether and when a campaign pays back, returns included.
The budget follows the numbers
Every morning the balance updates, and the decision on what to cut starts from the same data for everyone.

For the technical team

How it is built in Muvia

  1. 1

    Put spend and orders on the same hour

    The sources bring spend per campaign, the store's orders and the calendar; a query aligns them hour by hour in a dataset.

  2. 2

    Compute the baseline in Python

    A Python function builds, for every hour, the typical week without campaigns from the clean hours, corrected for season and holidays. The gap between actual and expected is the campaign's effect; margin and return lag are parameters.

  3. 3

    Schedule the recomputation

    A flow runs it every morning and writes to managed tables the hour-by-hour effect and each campaign's balance: spend, extra orders, extra margin and days to pay back.

  4. 4

    Align the campaigns in Analysis

    The Layers view overlays the campaigns on their launch moment: you see which channel responds within hours and which stays flat. The balance since launch shows when each one pays back its spend.

  5. 5

    Ask Athena what to cut

    Athena reads the campaign balance, saves the ranking in the Lab and writes in a notebook which campaign to cut and which to double.

Python function

import numpy as np
import pandas as pd

def transform(inputs, params, ctx):
    d = inputs["rows"].copy()
    d["hour"] = pd.to_datetime(d["hour"], utc=True).dt.tz_convert("Europe/Rome")
    d["hour_of_week"] = d["hour"].dt.weekday * 24 + d["hour"].dt.hour
    clean = d["campaign"].isna()
    # the typical week: the mean of each hour of the week over hours with no campaign
    typical = d[clean].groupby("hour_of_week")["orders"].mean()
    d["expected_orders"] = d["hour_of_week"].map(typical)
    d["extra_orders"] = np.where(clean, 0.0, d["orders"] - d["expected_orders"])
    basket = d["revenue"].sum() / d["orders"].sum()
    margin = d["extra_orders"] * basket * params["gross_margin_pct"] / 100
    d["campaign_balance"] = (margin - d["spend"]).groupby(d["campaign"]).cumsum()
    columns = ["hour", "campaign", "orders", "expected_orders", "extra_orders", "campaign_balance"]
    return {"rows": d[columns]}
A trimmed-down version: orders expected from a typical week without campaigns, the gap, and each campaign's balance.
The data it needs
  • Hourly spend per campaign and channel (email, social, search, TV), read through a REST API
  • Online store sessions, orders, revenue and returns, hour by hour, from the database on PostgreSQL
  • Campaign calendar with channel, start and end, from Google Sheets
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.