AI Engineering
/
AI Product & Experimentation
/

Design an A/B test with cost guardrails

AI Product & Experimentation · 2 to 3 hours

Design an A/B test with cost guardrails

Write the experiment contract and sample-size plan for a staged-hints A/B test, implement a validated per-arm summary of outcomes and costs, and explain which checks and guardrails must pass before a lift can justify shipping.

Medium
Hands-on

3 h

Used in interview rounds:

Live Coding (Medium)

Overview

Turn a product question into a testable A/B experiment: write the contract, design the events, summarize one outcome and cost record per assigned learner, and plan the statistics and guardrails that decide whether a lift means anything. The runnable summary is descriptive only: it does not randomize, impute missing outcomes, test sample ratio mismatch, estimate intervals, or make a ship decision.

Study time: 2 to 3 hours

Interview format: Hands-on implementation against fixtures, then an experiment design discussion

By the end you can

  • Write an experiment contract that fixes the population, assignment unit, metrics, tolerances, window, analysis, and decision owner before launch
  • Choose an intent-to-treat population and explain when exposure-based analyses are only secondary
  • Design assignment, exposure, and outcome events that deduplicate cleanly and join to the authoritative assignment record
  • Implement a validated per-arm summary of success rate, mean cost, cost per success, and lift that returns null instead of dividing by zero
  • Estimate sample size and test duration, and explain the sample ratio, uncertainty, and guardrail checks a ship decision needs

Prerequisites

Helpful background: Python or TypeScript for the summary function (the practice problem accepts JavaScript or Python), and comfort with rates and percentages.

This topic covers user outcomes, baselines, online experiments, cost guardrails, and ship decisions. Evaluation & Observability covers offline evaluation mechanics.

The staged-hints test

Worked example

Suppose a coding-practice platform answers a hint request with one full explanation from a language model it calls. The team has built staged hints on the same model: a nudge, then a targeted hint, then one worked step. They want to know whether staged hints help learners solve problems on their own without raising cost or answer disclosure beyond agreed limits. All figures are synthetic.

Planning inputValue
Eligible learnersSigned-in learners who start a supported beginner exercise
TrafficAbout 4,000 newly eligible learners per week
Allocation50% control (full explanation), 50% treatment (staged hints)
Primary outcomeSolves a related exercise without hints within 7 days of assignment
BaselineAbout 20% of eligible learners reach the primary outcome
Smallest effect worth acting on+2 percentage points on the primary outcome
Significance and power0.05 two-sided, 80% power
Cost guardrailMean model cost per assigned learner at most 25% above control
Quality guardrailShare of sampled hints judged incorrect in blinded review at most 1 percentage point above control
Not set yetTolerances for full-answer disclosure and hint latency

Model cost is recorded per learner in integer microunits (millionths) of one declared currency, including retries, escalations, and failed attempts.

Write the experiment contract

Write the contract before launch: hypothesis, eligibility, assignment unit, variants, primary outcome, guardrails with tolerances, observation window, statistical method, stopping rules, and decision owner. The smallest effect worth acting on sets the sample size and separates a real but trivial change from a useful one.

Randomize before the treatment can influence who is included, and keep each learner's assignment stable across sessions and devices.

Version the model, prompt, retrieval policy, and UI. If the treatment bundles several changes, the experiment estimates the bundle's effect and cannot isolate each part without additional design.

Define the analysis population

An intent-to-treat analysis compares eligible units by assigned variant, including those who never received the treatment. It estimates the effect of offering the experience under the real delivery process, failures included.

Exposure-based analyses can be useful, but conditioning on successful loading or later engagement may select different users in each arm, so treat them as secondary unless the causal design justifies them.

For delayed outcomes, wait until each cohort's observation window is complete, or use an appropriate time-to-event method. Never compare mature control users with newly enrolled treatment users.

Design the events

Use distinct events for assignment, exposure, request completion, feedback, and learning outcomes. Each carries the experiment ID, a pseudonymous unit ID, the variant, an event ID, a timestamp, and the relevant artifact and configuration revision.

Deduplicate events by their stable event ID. Preserve failures and missingness instead of turning them into successful no-op events. Check time zones, late arrivals, retries, cross-device identity, and reassignment after sign-in.

Join outcomes to the authoritative assignment record. A variant label sent by the client is not a source of truth on its own. Collect only the fields the analysis and operational investigation need.

Summarize one record per assigned learner

The function takes one row per assigned learner whose observation window has closed. success is true when the primary outcome occurred within the window. A learner who never came back has an observed outcome of false once the window closes; one whose events were lost, or whose window is still open, has a missing outcome, which is resolved upstream and never coded as false.

costMicrounits is the learner's total attributable model cost as an integer, including retries and failed attempts, and 0 for a learner who never asked for a hint. Integers avoid floating-point drift in currency totals.

type UserRecord = {
  userId: string;
  variant: "control" | "treatment";
  success: boolean;
  costMicrounits: number;
};

type ArmSummary = {
  users: number;
  successRate: number; // successes / users
  meanCostMicrounits: number; // total cost / users
  costPerSuccessMicrounits: number | null; // total cost / successes, null with 0 successes
};

declare function summarizeVariants(rows: UserRecord[]): {
  control: ArmSummary;
  treatment: ArmSummary;
  absoluteLiftPercentagePoints: number; // 100 * (treatment rate - control rate)
  relativeLift: number | null; // (treatment rate - control rate) / control rate, null when the control rate is 0
};

Python uses summarize_variants(rows) and snake_case keys (user_id, cost_microunits, success_rate, and so on).

Validate in this order and stop at the first failure (JavaScript throws an Error, Python raises ValueError):

OrderCheckMessage
1rows is a non-empty listExpected nonempty user records
2Each row is an object (a dict in Python)Record must be an object
3The user ID is a non-blank string not seen in either armMissing or duplicate user ID
4The variant is exactly control or treatmentUnknown variant
5success is a booleanOutcome must be observed boolean
6The cost is an integer of at least 0, not a booleanCost must be nonnegative integer
7After all rows, each arm has a learnerBoth variants need observations

Two fixtures, each listing user (success, cost in microunits), exercise the function:

FixtureControlTreatment
Au1 (true, 100), u2 (false, 300)u3 (true, 250), u4 (true, 350)
Bu5 (false, 100), u6 (false, 100)u7 (true, 200), u8 (false, 0)

IDs are compared exactly as given (no trimming or case folding), and keys outside the record are ignored.

Four learners per fixture check arithmetic and validation only, not whether staged hints work.

Code exercise

Per-arm outcome and cost summary

Implement summarizeVariants(rows) (Python summarize_variants(rows)) from the contract above. Run the seven checks in order and raise the first failing message, then return each arm's users, success rate, mean cost, and cost per success, plus the absolute and relative lift, with null (None in Python) wherever a denominator is 0.

Expected

The practice problem's tests compare the whole summary for fixtures A and B, check validation messages for invalid input such as duplicate or blank IDs, a capitalized variant, non-boolean outcomes, boolean, negative, or fractional costs, and a missing arm, and check that a ratio with a zero denominator comes back as null rather than 0, NaN, or Infinity. Floating-point results are compared after rounding to four decimal places.

Open coding workspace

Limitations

The function summarizes pre-aggregated, mature records for one experiment. It does not randomize, join events to assignments, impute missing outcomes, test sample ratio mismatch, estimate intervals or p-values, or make a ship decision. Python accepts only int costs while JavaScript cannot tell 100.0 from 100, so the fixtures use whole numbers. A production schema also scopes identity to the experiment and checks assignment consistency against the source registry.

Predict both summaries

Predict the outcome

Predict both summaries

Compute every output for fixtures A and B by hand, writing each ratio as a fraction first, before you reveal the answers. Costs are in microunits.

OutputFixture AFixture B
Control users??
Control success rate??
Control mean cost??
Control cost per success??
Treatment users??
Treatment success rate??
Treatment mean cost??
Treatment cost per success??
Absolute lift (percentage points)??
Relative lift??

Predict the validation results

Predict the outcome

Predict the validation results

Each row changes fixture A (rows u1 to u4, in that order). Predict which numbered check fails first, and so which message is raised, before you reveal the answers.

Change to fixture AFirst failing checkWhy
u3's user ID becomes "u1"??
u4's user ID is three spaces??
u3's variant is "Treatment"??
u2's success is null (its outcome event was lost)??
u2's success is the number 1??
u1's cost is the boolean true??
u1's cost is -1??
u1's cost is 1.5??
u2's variant is "beta" and its success is null??
u3 and u4 are removed??
All four rows are removed??
The string "u9" is appended as a fifth row??

Validate randomization and telemetry

Before reading any effect, compare observed assignment counts with the planned allocation. A sample ratio mismatch (SRM) is a gap that chance does not explain, usually checked with a chi-square test; the split need not be exactly 50/50. An SRM points to an assignment, logging, join, or filtering problem, and results are untrustworthy until the cause is found.

A/A checks, where both arms get the same experience, test instrumentation and false-positive behavior, though one passing A/A test does not prove the pipeline correct. Pre-treatment balance and invariant metrics are further diagnostics.

Compare assignment, exposure, and outcome counts per arm: differential missingness can bias results even after balanced assignment. Never delete inconvenient rows until the arms look similar.

Plan the statistics

Analyze at the assignment unit. Repeated hint requests from one learner are correlated, so treating each request as an independent unit understates uncertainty; aggregate to one row per learner or use a clustered method.

Size the test from the baseline rate, the smallest effect worth acting on, the significance level, and the power. For a binary outcome with two equal arms, a common normal-approximation planning estimate is:

Sample size per arm (normal approximation)
n = (z_alpha + z_power)^2 * (p_c * (1 - p_c) + p_t * (1 - p_t)) / d^2

p_c baseline (control) rate
d smallest absolute difference worth acting on
p_t p_c + d, the treatment rate the test must be able to detect
z_alpha 1.96 for a two-sided test at 0.05
z_power 0.84 for 80% power

Round n up, divide by the learners each arm receives per week, and add the observation window, since the last learner enrolled still needs a complete window. Formula variants (pooled variance, continuity correction) shift the result slightly; clustered or skewed metrics need more units.

Report an interval for every effect, not only a point estimate. Predefine the primary metric and how multiple comparisons are handled, and treat surprising subgroup results as hypotheses to confirm. Checking a fixed-horizon test daily and stopping at the first significant result inflates false positives: run to the planned size or use a sequential method chosen in advance.

Set quality and cost guardrails

A guardrail needs a tolerance and evidence that rules out exceeding it. A non-significant increase in incorrect hints is not proof that quality held, because the interval may still include meaningful harm; the guardrail passes when the upper end of that interval sits inside the tolerance.

Judge sampled quality with blinded human review or independent execution checks, keep the sampling design so easy cases are not overrepresented, and track reviewer disagreement.

Count every attributable cost: retries, escalation, tools, and review. Report mean cost per assigned learner, the heavy-user distribution, and total spend. Cost per success is a ratio of totals, not the causal cost of one more success, and it is unstable when successes are rare.

Analyze, decide, and roll out

Read results in order: data validity, primary effect, guardrails, operational feasibility, then predefined slices. Distinguish "failed to show benefit" from "showed no meaningful benefit" and "showed harm".

A decision memo gives effect estimates with uncertainty, absolute counts, delivery rates, limitations, and the action, and says whether it covers all users or a supported segment. Do not change the hypothesis after seeing results; write a new one and test it.

After a successful test, roll out gradually with rollback criteria, monitoring, and a holdout when longer-term effects matter. Population shifts and model updates can alter the effect, so carry the experiment's limitations into the release record.

Review a draft analysis plan

Apply the population, event, statistics, and guardrail sections to a teammate's draft before any data arrives.

Diagnose

Review a draft analysis plan

A teammate drafts the analysis for the staged-hints test:

  1. Build the analysis table from hint-request logs, one row per request, using the variant label the browser sends with each request.
  2. In treatment, include only learners whose staged hints loaded without an error.
  3. Give learners with no outcome event success: false.
  4. Sum cost from successful model responses only.
  5. Check the dashboard every morning and stop as soon as the primary lift is significant at 0.05.
  6. If the primary outcome does not move, report whichever of 20 learner segments improved most as the result.
  1. For each step, name the bias or error it introduces.
  2. Rewrite the step so the analysis stays valid.
  3. Say which corrections the summary function's contract already enforces and which must happen upstream or in the analysis plan.

Completion checklist

  • I can reproduce the fixture A and B summaries and explain each ratio whose denominator is 0.
  • I can write a contract that fixes population, assignment unit, metrics, tolerances, window, analysis, and decision owner before launch.
  • I can estimate sample size and duration from a baseline, a smallest worthwhile effect, significance, power, and weekly traffic.
  • I can list the validity checks that must pass before reading a lift, and what each failure blocks.

Sources

The platform, planning inputs, tolerances, records, fixtures, and rubric are original synthetic teaching material about a fictional application. Only synthetic arithmetic and validation are tested; no real user experiment was run and no statistical significance claim is made. Study times and readiness thresholds are editorial estimates.

Practice the interview question

Answer it in your own words before you look at the reference answer.