Data-Prep Cookbook

You have a Google Ads export, a Meta export, and a GA4 sales pull — three CSVs at three cadences with three schemas. This recipe joins them into the one Master Flat File (MFF) the framework models, and hands the result to load_mff. The platform connectors automate this for hosted users; this page is the code version for anyone starting from raw files.

The target: one long table

Everything below builds toward MFF long format — one row per variable × period (× geography), with a Period column, the dimension columns, and a VariableName / VariableValue pair. Sales, each channel's spend, and each control all live in the same table, distinguished only by VariableName. The column dictionary is the authoritative spec.

1 · Normalize each export

Platform exports rarely share a schema. For each source, keep only what you need and rename to a common shape: a week date column plus one numeric column per metric.

# illustrative — plain pandas, adapt column names to your exports
import pandas as pd

# Google Ads: daily rows -> keep date + cost + impressions, rename channel
gads = pd.read_csv("google_ads.csv", parse_dates=["day"])
gads = gads.rename(columns={"day": "date", "cost": "search_spend",
                            "impressions": "search_impr"})

# Meta: same treatment
meta = pd.read_csv("meta.csv", parse_dates=["date"])
meta = meta.rename(columns={"amount_spent": "social_spend",
                            "impressions": "social_impr"})

# GA4: weekly sales (the KPI)
sales = pd.read_csv("ga4_revenue.csv", parse_dates=["week"])
sales = sales.rename(columns={"week": "date", "revenue": "Sales"})

2 · Align the cadence

MMM is almost always fit weekly. Roll daily platform data up to the same weekly anchor as your KPI (if Sales is reported for weeks ending Sunday, use Sundays everywhere). Sum spend and impressions within the week; average or take period-end for a price or index control.

# illustrative — resample daily platform data to weekly (week ending Sunday)
def to_weekly(df, sum_cols):
    return (df.set_index("date")
              .resample("W-SUN")[sum_cols].sum()
              .reset_index())

gads_w = to_weekly(gads, ["search_spend", "search_impr"])
meta_w = to_weekly(meta, ["social_spend", "social_impr"])
# sales is already weekly; make sure its dates land on the same W-SUN anchor

Anchor consistency matters

The loader anchors its expected weekly grid to your data's own weekday, so Monday- or Sunday-dated weeks both validate — but every variable must share one anchor. Mixed anchors read as missing weeks. Pick one and resample all sources to it.

3 · Join & fill gaps

Outer-join the weekly frames on date so no week is silently dropped, then decide each column's fill rule: a channel that was simply dark that week is a real zero; a control that's genuinely unobserved is forward-filled or interpolated; a missing KPI week is a data problem to fix, not to zero.

# illustrative
wide = (sales
        .merge(gads_w, on="date", how="outer")
        .merge(meta_w, on="date", how="outer")
        .sort_values("date"))

# spend/impressions: a missing week means the channel was off -> 0
media_cols = ["search_spend", "search_impr", "social_spend", "social_impr"]
wide[media_cols] = wide[media_cols].fillna(0.0)

# KPI must be present every week — investigate, don't fillna
assert wide["Sales"].notna().all(), "missing KPI weeks — fix upstream before modeling"

The framework will warn you about non-contiguous weeks at load time, but it's cheaper to resolve gaps here. See Troubleshooting → Data for the exact messages.

4 · Spend or impressions?

Model each channel on the variable that actually drives response. Spend is the usual choice and lets ROI divide by dollars directly. If you model impressions or clicks, the framework computes efficiency-per-unit instead — and can still report dollar ROI if you attach a spend column or a CPM/CPC. Keep both columns in the wide frame and decide per channel; the spend-vs-impressions section covers the trade-off.

5 · Emit the MFF and load it

Melt the wide frame to MFF long format with mff_from_wide_format — you pass a mapping of your column names to canonical VariableNames — then declare roles with MFFConfigBuilder and load:

from mmm_framework import mff_from_wide_format, MFFConfigBuilder, load_mff

# wide columns -> canonical variable names (modeling on spend here)
mff = mff_from_wide_format(
    wide,
    period_col="date",
    value_columns={
        "Sales": "Sales",
        "search_spend": "Search",
        "social_spend": "Social",
    },
)

config = (
    MFFConfigBuilder()
    .with_kpi_name("Sales")
    .add_national_media("Search", adstock_lmax=4)
    .add_national_media("Social", adstock_lmax=6)
    .weekly()
    .build()
)

panel = load_mff(mff, config)
print(panel.summary())

That panel is the exact object the quickstart fits. Want to rehearse the downstream steps first? load_example("national") returns a ready panel of the same shape.

Pre-flight checklist

Before you fit, confirm:

Then fit

With a clean panel, follow the step-by-step build, then read the output with the report-interpretation guide.