Skip to content

babelfin

Analysts import babelfin as fin

Babelfin is the front door to UMP for notebooks and scripts. Put a number through it and it reads the same in every tool.

It is a thin layer over ump-spec. Every result keeps the underlying contract object on .spec, so nothing about the standard changes underneath. Services never import it.

  1. Install: pip install -e 'packages/babelfin[recipe,plot]' from a checkout (recipe adds finprint, plot adds matplotlib).
  2. Import: import babelfin as fin.
  3. Build an estimand, then a number. Every number needs an interval.
import babelfin as fin
roi = fin.estimand("roi", channels=["tv"], weeks=104)
tv = fin.number(1.37, (1.26, 1.50), roi, label="TV ROI")
tv
# <Number TV ROI: 1.37 (90% 1.26 to 1.50)>

tv.explain() spells out the question the number answers: revenue per dollar, net, from removing all TV spend versus zero spend, over 104 weeks, posterior median, 90% equal-tailed interval.

CallWhat it does
fin.estimand(name, **changes)Starts from a registered question (roi, mroi, iroas, lift or any registry id) and changes only what differs.
fin.number(value, (lo, hi), estimand)A shown number. The interval is required, so a bare point cannot slip through.
fin.compare(a, b) / fin.total({...})Walks the estimand alignment ladder (gate G-EST-02) and sums channels inside the dependence envelope (G-DEP-01).
fin.check(kind, doc)Runs the gates on a document offline and returns a report with its fallbacks as a table.
fin.Workflow(...)Fits your own model functions, asks named questions and saves the lot as a workflow bundle.
fin.connect(as_=...)A session against a running core: file, submit and approve artifacts, browse models, plan and prioritise tests.

Two numbers with different estimands answer different questions. Babelfin never averages them silently.

geo = fin.number(0.95, (0.61, 1.30), fin.estimand("iroas", channels=["tv"]), label="TV iROAS, geo test")
fin.compare(tv, geo)
# <Alignment assumed_alignment; differs on intervention, baseline, horizon, point_rule, interval>

Here the two differ only on fields the ladder may assume, so the comparison lands on the assumed-alignment rung and widens the interval to the envelope across plausible alignments. Pass strict=True to raise fin.Stopped instead.

fin.total({"tv": tv, "search": (3.2, 2.7, 3.8)})
# <Total 4.57 (3.96 to 5.30, per_channel_envelope)>

Without joint draws, the total runs on the independence-to-comonotone envelope. Pass a DataFrame of draws and it uses them.

Rules babelfin keeps
  • A vendor number with no interval goes through fin.imputed_number(value, "roi"), which imputes a band and labels it as imputed.
  • fin.ladder(available) picks the highest rung of the fallback ladder: full model, curve parameters, spend table, summary (warned), benchmarks (warned), not available.
  • Every result has the same small surface: .spec, .to_frame() where there is a table, .to_dict(), .explain(), and a card in Jupyter with the headline first and the rigour behind a disclosure.
report = fin.check("model_manifest", doc) # at="submit" by default, or "seal"
report.ok
report.fallbacks.to_frame()
report.raise_for_stops()

fin.validate(kind, doc) returns the typed model or raises fin.InvalidDocument, whose .to_frame() lists every issue with its pointer.

c = fin.Contract.from_frame(train, time="week", target="sales", freq="W-SUN")
wf = fin.Workflow("Weekly sales", contract=c, code="sales_model.py", seed=11, params={"draws": 200})
wf.question("tv_roi", ask="What does a dollar of TV return?", estimand=fin.estimand("roi", channels=["tv"]))
fitted = wf.fit(train)
fitted.answer("tv_roi") # a fin.Number with its estimand
fitted.save("out/weekly_sales", include_data=False)
lw = fin.Workflow.load("out/weekly_sales") # hashes checked, nothing run
lw.code # read it, then trust it
lw.trust()
run = lw.run(new_df) # scores by default
run.route, run.source, run.answers()

sales_model.py holds plain functions: fit, to_state, from_state and one function per question. The ump-workflow guide explains the bundle format and when a run refits.

Talk to a running core
with fin.connect(as_="maya.chen") as s:
s.check(model) # server dry run; a stop raises fin.Stopped
art = s.file(model).submit(reason="Q1 refresh")
with s.as_("sam.patel") as sam: # a second person approves, never the author
sam.artifact(art.id).approve(accept=art.fallbacks)
m = s.models.get(art.id)
opt = s.planner.optimize(m, max_change=0.30) # opt.uplift is a fin.Number

Approving requires naming every fallback the artifact used, or the call is refused. Approving your own work raises fin.Forbidden (gate G-INT-02).