axiom

A model of your own · step 5 of 5

What the custom route costs you

You left the kernels behind. What did you leave with them?

Some of the library follows a hand-built model and some of it does not. design.profile_likelihood takes a ModelSpec directly, so the identification and design tools that work on a likelihood still apply — that is how GEIGER-1911 gets its one-sided bound.

What you give up is the SurfaceSpec conveniences: surface.fit, and estimands.realize reading an estimand off the result, both want the richer object. If a kernel can express your response, use one. Write the mean when the mechanism genuinely is not on the menu — and then you are in the same position as a physics case study, which is a respectable place to be.

step 5
import warnings
from axiom.design import profile_likelihood

data = {"spend": x, "footfall": y}
start = {**TRUE}
with warnings.catch_warnings():
    warnings.simplefilter("ignore", RuntimeWarning)
    values, drops = profile_likelihood(
        model, data, start, "k", grid=np.linspace(180.0, 380.0, 21)
    )

drops = np.asarray(drops)
inside = [v for v, d in zip(values, drops) if d <= 1.921]
print("profiling k, the spend at which most of the lift has arrived:")
print(f"  best fit          {values[int(np.argmin(drops))]:.0f}")
print(f"  95% interval      {min(inside):.0f} to {max(inside):.0f}")
print(f"  the truth         {TRUE['k']:.0f}")
print()
print("that ran on the ModelSpec itself. surface.fit and")
print("estimands.realize did not -- they want a SurfaceSpec.")
what it printed
profiling k, the spend at which most of the lift has arrived:
  best fit          240
  95% interval      210 to 270
  the truth         260

that ran on the ModelSpec itself. surface.fit and
estimands.realize did not -- they want a SurfaceSpec.

Every line above was captured by running this code at build time. The steps share one namespace, so the code on this page is the code you would type after the steps before it.