import%20marimo%0A%0A__generated_with%20%3D%20%220.25.0%22%0Aapp%20%3D%20marimo.App(width%3D%22medium%22)%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20pandas%20as%20pd%0A%0A%20%20%20%20from%20increment%20import%20Analysis%2C%20Method%2C%20MetricSpec%0A%20%20%20%20from%20increment.estimation.inference%20import%20Normal%0A%20%20%20%20from%20increment.results%20import%20LiftEstimate%0A%20%20%20%20from%20increment.tables%20import%20estimates_to_readout%2C%20readout_table%0A%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20Analysis%2C%0A%20%20%20%20%20%20%20%20LiftEstimate%2C%0A%20%20%20%20%20%20%20%20Method%2C%0A%20%20%20%20%20%20%20%20MetricSpec%2C%0A%20%20%20%20%20%20%20%20Normal%2C%0A%20%20%20%20%20%20%20%20estimates_to_readout%2C%0A%20%20%20%20%20%20%20%20mo%2C%0A%20%20%20%20%20%20%20%20np%2C%0A%20%20%20%20%20%20%20%20pd%2C%0A%20%20%20%20%20%20%20%20readout_table%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Analysing%20an%20experiment%20straight%20from%20a%20dataframe%0A%0A%20%20%20%20While%20the%20core%20workflow%20of%20this%20package%20is%20connecting%20straight%20to%20a%20warehouse%2C%20we%20also%20support%20analysis%20straight%20from%20a%20dataframe%2C%20making%20it%20easy%20for%20data%20scientists%20to%20do%20adhoc%20analyses.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20An%20Example%20Checkout%20Experiment%0A%0A%20%20%20%20We%20simulated%20a%20fake%20experiment%20with%20the%20following%20metrics%3A%0A%20%20%20%20*%20revenue%20over%20the%205-day%20experiment%20window%0A%20%20%20%20*%20revenue%20over%20the%205%20days%20*before*%20the%20experiment%20(the%20CUPED%20covariate)%0A%20%20%20%20*%20an%20order%20count%20(for%20a%20ratio%20metric)%0A%20%20%20%20*%20and%20a%20pre-computed%20conversion%20indicator%0A%0A%20%20%20%20The%20treatment%20raises%20the%20value%20of%20each%20order%20by%2015%25%20and%20touches%20nothing%0A%20%20%20%20else%2C%20so%20order%20counts%20carry%20no%20true%20effect.%20One%20row%20per%20user%2C%20exactly%20what%0A%20%20%20%20%60from_unit_summary%60%20expects.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20simulate(np%2C%20pd)%3A%0A%20%20%20%20_rng%20%3D%20np.random.default_rng(7)%0A%20%20%20%20_n_users%20%3D%2020_000%0A%20%20%20%20_n_days%20%3D%205%0A%0A%20%20%20%20%23%20Persistent%20per-user%20traits%3A%20they%20drive%20both%20the%20pre-period%20and%20the%0A%20%20%20%20%23%20experiment%20window%2C%20which%20is%20what%20makes%20pre_revenue%20a%20useful%20covariate.%0A%20%20%20%20_quality%20%3D%20_rng.normal(0%2C%201%2C%20_n_users)%0A%20%20%20%20_countries%20%3D%20_rng.choice(%5B%22US%22%2C%20%22CA%22%2C%20%22GB%22%5D%2C%20size%3D_n_users%2C%20p%3D%5B0.5%2C%200.25%2C%200.25%5D)%0A%20%20%20%20_country_scale%20%3D%20np.select(%0A%20%20%20%20%20%20%20%20%5B_countries%20%3D%3D%20%22CA%22%2C%20_countries%20%3D%3D%20%22GB%22%5D%2C%0A%20%20%20%20%20%20%20%20%5B0.9%2C%201.1%5D%2C%0A%20%20%20%20%20%20%20%20default%3D1.0%2C%0A%20%20%20%20)%0A%20%20%20%20_active_probability%20%3D%20np.clip(0.35%20%2B%200.15%20*%20_quality%2C%200.05%2C%200.95)%0A%20%20%20%20_order_rate%20%3D%20np.clip(0.4%20%2B%200.2%20*%20_quality%2C%200.05%2C%20None)%0A%20%20%20%20_order_value%20%3D%20(10%20%2B%204%20*%20_quality)%20*%20_country_scale%0A%0A%20%20%20%20%23%20One%20population%20randomized%20into%20arms%3B%20the%20lift%20applies%20to%20order%20value%20only.%0A%20%20%20%20_variant%20%3D%20np.where(_rng.random(_n_users)%20%3C%200.5%2C%20%22treatment%22%2C%20%22control%22)%0A%20%20%20%20_lift%20%3D%20np.where(_variant%20%3D%3D%20%22treatment%22%2C%200.15%2C%200.0)%0A%0A%20%20%20%20def%20_simulate_day(rng%2C%20lift)%3A%0A%20%20%20%20%20%20%20%20active%20%3D%20rng.uniform(0%2C%201%2C%20_n_users)%20%3C%20_active_probability%0A%20%20%20%20%20%20%20%20orders%20%3D%20np.where(active%2C%201%20%2B%20rng.poisson(_order_rate)%2C%200)%0A%20%20%20%20%20%20%20%20value%20%3D%20np.clip((_order_value%20%2B%20rng.normal(0%2C%202%2C%20_n_users))%20*%20(1%20%2B%20lift)%2C%200%2C%20None)%0A%20%20%20%20%20%20%20%20return%20orders%2C%20orders%20*%20value%0A%0A%20%20%20%20%23%20Pre-period%3A%20same%20users%2C%20same%20process%2C%20before%20anyone%20was%20treated.%0A%20%20%20%20_pre_revenue%20%3D%20np.zeros(_n_users)%0A%20%20%20%20for%20_day%20in%20range(_n_days)%3A%0A%20%20%20%20%20%20%20%20_pre_revenue%20%2B%3D%20_simulate_day(_rng%2C%200.0)%5B1%5D%0A%0A%20%20%20%20_daily_frames%20%3D%20%5B%5D%0A%20%20%20%20for%20_day%20in%20range(_n_days)%3A%0A%20%20%20%20%20%20%20%20_orders%2C%20_revenue%20%3D%20_simulate_day(_rng%2C%20_lift)%0A%20%20%20%20%20%20%20%20_daily_frames.append(%0A%20%20%20%20%20%20%20%20%20%20%20%20pd.DataFrame(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22user_id%22%3A%20%5Bf%22u%7Bi%3A05d%7D%22%20for%20i%20in%20range(_n_users)%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22variant%22%3A%20_variant%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22day%22%3A%20f%222026-02-%7B_day%20%2B%201%3A02d%7D%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22country%22%3A%20_countries%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22revenue%22%3A%20_revenue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22orders%22%3A%20_orders%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22converted%22%3A%20(_orders%20%3E%200).astype(float)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22pre_revenue%22%3A%20_pre_revenue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20panel_df%20%3D%20pd.concat(_daily_frames%2C%20ignore_index%3DTrue)%0A%0A%20%20%20%20checkout_df%20%3D%20panel_df.groupby(%5B%22user_id%22%2C%20%22variant%22%2C%20%22country%22%5D%2C%20as_index%3DFalse).agg(%0A%20%20%20%20%20%20%20%20revenue%3D(%22revenue%22%2C%20%22sum%22)%2C%0A%20%20%20%20%20%20%20%20pre_revenue%3D(%22pre_revenue%22%2C%20%22first%22)%2C%0A%20%20%20%20%20%20%20%20orders%3D(%22orders%22%2C%20%22sum%22)%2C%0A%20%20%20%20%20%20%20%20converted%3D(%22converted%22%2C%20%22max%22)%2C%0A%20%20%20%20)%0A%20%20%20%20checkout_df.head().round(2)%0A%20%20%20%20return%20checkout_df%2C%20panel_df%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20The%20core%20workflow%0A%0A%20%20%20%20Analyzing%20an%20experiment%20is%20easy%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Analysis%2C%20checkout_df%2C%20estimates_to_readout%2C%20readout_table)%3A%0A%20%20%20%20%23%20pass%20in%20your%20experiment%0A%20%20%20%20baseline%20%3D%20Analysis.from_unit_summary(%0A%20%20%20%20%20%20%20%20checkout_df%2C%0A%20%20%20%20%20%20%20%20unit%3D%22user_id%22%2C%0A%20%20%20%20%20%20%20%20group%3D%22variant%22%2C%0A%20%20%20%20%20%20%20%20control%3D%22control%22%2C%0A%20%20%20%20%20%20%20%20metrics%3D%7B%22revenue%22%3A%20%22mean%22%2C%20%22converted%22%3A%20%22conversion%22%7D%2C%0A%20%20%20%20)%0A%20%20%20%20baseline_results%20%3D%20baseline.run()%0A%0A%20%20%20%20%23%20visualize%20the%20readout%0A%20%20%20%20readout_table(estimates_to_readout(baseline_results)%2C%20title%3D%22Checkout%20experiment%22)%0A%20%20%20%20return%20(baseline%2C)%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20CUPED%3A%20adjusting%20for%20the%20pre-period%20covariate%0A%0A%20%20%20%20CUPED%20is%20also%20implemented.%20With%20just%20one%20call%2C%20both%20methods%20are%20supported.%0A%20%20%20%20The%20moments%20are%20computed%20once%2C%20and%20the%20CUPED%20adjustment%20is%20applied%20to%20the%0A%20%20%20%20same%20per-arm%20sums%20the%20unadjusted%20estimate%20uses%2C%20so%20any%20difference%20below%20is%0A%20%20%20%20the%20adjustment%20and%20nothing%20else.%20How%20much%20it%20buys%20is%20set%20by%20one%20number%3A%20the%0A%20%20%20%20correlation%20between%20the%20covariate%20and%20the%20metric.%20Variance%20falls%20by%20roughly%0A%20%20%20%20that%20correlation%20squared%2C%20so%20a%20weak%20covariate%20is%20not%20worth%20the%20column.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20cuped(%0A%20%20%20%20Analysis%2C%0A%20%20%20%20Method%2C%0A%20%20%20%20MetricSpec%2C%0A%20%20%20%20checkout_df%2C%0A%20%20%20%20estimates_to_readout%2C%0A%20%20%20%20readout_table%2C%0A)%3A%0A%20%20%20%20UNADJUSTED%20%3D%20Method(name%3D%22unadjusted%22)%0A%20%20%20%20CUPED%20%3D%20Method(name%3D%22cuped%22%2C%20variance_reduction%3D%22cuped%22)%0A%0A%20%20%20%20cuped_analysis%20%3D%20Analysis.from_unit_summary(%0A%20%20%20%20%20%20%20%20checkout_df%2C%0A%20%20%20%20%20%20%20%20unit%3D%22user_id%22%2C%0A%20%20%20%20%20%20%20%20group%3D%22variant%22%2C%0A%20%20%20%20%20%20%20%20control%3D%22control%22%2C%0A%20%20%20%20%20%20%20%20metrics%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20MetricSpec(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22revenue%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20type%3D%22mean%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20covariate%3D%22pre_revenue%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20)%0A%20%20%20%20cuped_estimates%20%3D%20cuped_analysis.run(decision_method%3DUNADJUSTED%2C%20sensitivity_methods%3D(CUPED%2C))%0A%20%20%20%20readout_table(estimates_to_readout(cuped_estimates)%2C%20title%3D%22CUPED%20Comparison%22%2C%20nest_by%3D%22method%22)%0A%20%20%20%20return%20(cuped_estimates%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(LiftEstimate%2C%20checkout_df%2C%20cuped_estimates%2C%20mo)%3A%0A%20%20%20%20def%20interval_width(estimate%3A%20LiftEstimate)%20-%3E%20float%3A%0A%20%20%20%20%20%20%20%20lift%20%3D%20estimate.lift%0A%20%20%20%20%20%20%20%20assert%20lift.lb%20is%20not%20None%20and%20lift.ub%20is%20not%20None%2C%20%22run()%20always%20returns%20an%20interval%22%0A%20%20%20%20%20%20%20%20return%20lift.ub%20-%20lift.lb%0A%0A%20%20%20%20unadjusted_estimate%20%3D%20cuped_estimates%5B0%5D%0A%20%20%20%20cuped_estimate%20%3D%20cuped_estimates%5B1%5D%0A%20%20%20%20_plain_width%20%3D%20interval_width(unadjusted_estimate)%0A%20%20%20%20_adjusted_width%20%3D%20interval_width(cuped_estimate)%0A%20%20%20%20_rho%20%3D%20checkout_df%5B%22revenue%22%5D.corr(checkout_df%5B%22pre_revenue%22%5D)%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%60pre_revenue%60%20correlates%20with%20%60revenue%60%20at%20**%7B_rho%3A.2f%7D**%2C%20so%20the%20ceiling%20on%0A%20%20%20%20variance%20reduction%20is%20about%20%7B_rho**2%3A.0%25%7D%20%E2%80%94%20and%20CUPED%20narrows%20the%20interval%20by%0A%20%20%20%20**%7B(1%20-%20_adjusted_width%20%2F%20_plain_width)%3A.1%25%7D**%20(width%20shrinks%20with%20the%20square%0A%20%20%20%20root%20of%20variance).%20The%20point%20estimate%20moves%20from%0A%20%20%20%20**%7Bunadjusted_estimate.lift.value%3A%2B.1%25%7D**%20to%20**%7Bcuped_estimate.lift.value%3A%2B.1%25%7D**%3A%0A%20%20%20%20the%20arms%20differ%20slightly%20in%20pre-period%20revenue%20by%20chance%2C%20and%20the%20adjustment%0A%20%20%20%20prices%20that%20imbalance%20out.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Adding%20a%20prior%0A%0A%20%20%20%20Priors%20on%20the%20lift%20can%20be%20set%20via%0A%0A%20%20%20%20%60%60%60python%0A%20%20%20%20analysis.run(prior%3DNormal(mu%3Dmu%2C%20sigma%3Dsigma))%0A%20%20%20%20%60%60%60%0A%0A%20%20%20%20This%20is%20especially%20helpful%20for%20smaller%20sized%20experiments%20-%20and%20if%20your%20prior%20is%20well%20calibrated%20over%20historical%20experiments%20(which%20is%20no%20easy%20task)%2C%20it%20can%20help%20resolve%20peeking%20and%20multiple%20comparison%20issues.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Normal%2C%20baseline%2C%20estimates_to_readout%2C%20readout_table)%3A%0A%20%20%20%20prior_results%20%3D%20baseline.run(prior%3DNormal(mu%3D0.0%2C%20sigma%3D0.05))%0A%20%20%20%20readout_table(%0A%20%20%20%20%20%20%20%20estimates_to_readout(prior_results%2C%20informative_prior%3DTrue)%2C%0A%20%20%20%20%20%20%20%20title%3D%22Checkout%20experiment%20%E2%80%94%20informative%20prior%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20A%20ratio%20metric%0A%0A%20%20%20%20%60orders%60%20is%20the%20denominator%3A%20%60MetricSpec(type%3D%22ratio%22%2C%20numerator%3D...%2C%0A%20%20%20%20denominator%3D...)%60%20estimates%20lift%20on%20**revenue%20per%20order**%2C%20not%20on%20revenue%0A%20%20%20%20or%20order%20count%20separately.%0A%0A%20%20%20%20%60Revenue%20per%20User%60%20below%20is%20the%20same%20estimand%20as%20the%20%60revenue%60%20row%20in%20the%0A%20%20%20%20first%20table%20%E2%80%94%20a%20%60mean%60%20metric%20is%20already%20a%20per-unit%20average%2C%20so%20the%20two%0A%20%20%20%20always%20agree.%20It%20sits%20here%20to%20show%20the%20decomposition%3A%20revenue%20per%20user%20is%0A%20%20%20%20revenue%20per%20order%20times%20orders%20per%20user%2C%20and%20the%20three%20lifts%20multiply%20out%0A%20%20%20%20the%20same%20way.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Analysis%2C%20MetricSpec%2C%20checkout_df%2C%20estimates_to_readout%2C%20readout_table)%3A%0A%20%20%20%20ratio_result%20%3D%20Analysis.from_unit_summary(%0A%20%20%20%20%20%20%20%20checkout_df%2C%0A%20%20%20%20%20%20%20%20unit%3D%22user_id%22%2C%0A%20%20%20%20%20%20%20%20group%3D%22variant%22%2C%0A%20%20%20%20%20%20%20%20control%3D%22control%22%2C%0A%20%20%20%20%20%20%20%20metrics%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20MetricSpec(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22Avg.%20Order%20Value%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20type%3D%22ratio%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20numerator%3D%22revenue%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20denominator%3D%22orders%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20MetricSpec(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22Revenue%20per%20User%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20type%3D%22mean%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20value_column%3D%22revenue%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20MetricSpec(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22Avg.%20Orders%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20type%3D%22mean%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20value_column%3D%22orders%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20).run()%0A%0A%20%20%20%20readout_table(%0A%20%20%20%20%20%20%20%20estimates_to_readout(ratio_result)%2C%0A%20%20%20%20%20%20%20%20title%3D%22Analyzing%20a%20Ratio%20Metric%22%2C%0A%20%20%20%20%20%20%20%20%23%20nest_by%3D'method'%0A%20%20%20%20)%0A%20%20%20%20return%20(ratio_result%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo%2C%20ratio_result)%3A%0A%20%20%20%20_lift%20%3D%20%7Bestimate.metric%3A%20estimate.lift.value%20for%20estimate%20in%20ratio_result%7D%0A%20%20%20%20_implied%20%3D%20(1%20%2B%20_lift%5B%22Avg.%20Order%20Value%22%5D)%20*%20(1%20%2B%20_lift%5B%22Avg.%20Orders%22%5D)%20-%201%0A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20The%20treatment%20only%20raises%20the%20value%20of%20each%20order%2C%20and%20that%20is%20what%20the%0A%20%20%20%20ratio%20metric%20isolates%3A%20**%7B_lift%5B%22Avg.%20Order%20Value%22%5D%3A%2B.1%25%7D**%20on%20revenue%20per%0A%20%20%20%20order%20against%20**%7B_lift%5B%22Avg.%20Orders%22%5D%3A%2B.1%25%7D**%20on%20orders%20per%20user%2C%20whose%0A%20%20%20%20interval%20covers%20zero.%20Multiplying%20them%20gives%20**%7B_implied%3A%2B.1%25%7D**%2C%20matching%0A%20%20%20%20the%20**%7B_lift%5B%22Revenue%20per%20User%22%5D%3A%2B.1%25%7D**%20measured%20directly%20on%20revenue%20per%0A%20%20%20%20user.%20Revenue%20per%20user%20is%20the%20blunter%20metric%20here%3A%20it%20carries%20the%20order-count%0A%20%20%20%20noise%20on%20top%20of%20the%20effect.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Sample-ratio%20mismatch%0A%0A%20%20%20%20Free%20on%20this%20shape%3A%20%60srm()%60%20reads%20the%20per-group%20unit%20count%20straight%20out%20of%0A%20%20%20%20the%20moments%20already%20computed%20above%2C%20no%20extra%20pass%20over%20the%20data.%20Its%0A%20%20%20%20anytime-valid%20default%20requires%20known%2C%20constant%20conditional%20assignment%0A%20%20%20%20probabilities%3B%20%60expected%60%20declares%20their%20arm%20support%20and%20shares.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(baseline)%3A%0A%20%20%20%20from%20rich.pretty%20import%20pprint%0A%0A%20%20%20%20srm_result%20%3D%20baseline.srm(expected%3D%7B%22control%22%3A%200.5%2C%20%22treatment%22%3A%200.5%7D)%0A%20%20%20%20pprint(srm_result%2C%20expand_all%3DTrue)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Analyzing%20a%20panel%20dataset%0A%0A%20%20%20%20%60Analysis.from_unit_panel%60%20takes%20the%20same%20experiment%20used%20above%20at%20user-day%20grain.%0A%20%20%20%20The%20unit-summary%20examples%20aggregate%20this%20panel%20to%20one%20row%20per%20user%2C%20so%20their%0A%20%20%20%20whole-window%20effects%20match%20exactly.%20%60country%60%20is%20a%20stable%20per-user%20dimension%0A%20%20%20%20carried%20on%20every%20daily%20row.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(panel_df)%3A%0A%20%20%20%20panel_df.head().round(2)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Analysis%2C%20estimates_to_readout%2C%20mo%2C%20panel_df%2C%20readout_table)%3A%0A%20%20%20%20panel_analysis%20%3D%20Analysis.from_unit_panel(%0A%20%20%20%20%20%20%20%20panel_df%2C%0A%20%20%20%20%20%20%20%20unit%3D%22user_id%22%2C%0A%20%20%20%20%20%20%20%20group%3D%22variant%22%2C%0A%20%20%20%20%20%20%20%20date%3D%22day%22%2C%0A%20%20%20%20%20%20%20%20control%3D%22control%22%2C%0A%20%20%20%20%20%20%20%20metrics%3D%7B%22revenue%22%3A%20%22mean%22%7D%2C%0A%20%20%20%20)%0A%20%20%20%20daily_values%20%3D%20panel_analysis.run_daily()%0A%20%20%20%20panel_results%20%3D%20panel_analysis.run()%0A%20%20%20%20panel_readout%20%3D%20readout_table(%0A%20%20%20%20%20%20%20%20estimates_to_readout(panel_results)%2C%0A%20%20%20%20%20%20%20%20title%3D%22Analyzing%20Panel%20Data%22%2C%0A%20%20%20%20)%0A%20%20%20%20mo.vstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20panel_readout%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mo.md(f%22%60run_daily()%60%20produced%20**%7Blen(daily_values)%7D**%20per-day%20metric%20values.%22)%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Windows%2C%20retention%2C%20and%20censoring%20on%20the%20panel%20path%0A%0A%0A%20%20%20%20-%20Any%20windowed%20or%20retention%20metric%20needs%20%60exposure_date%3D...%60%20which%20is%20day%200%20for%20that%0A%20%20%20%20%20%20metric's%20window%2Fband%20arithmetic.%0A%0A%20%20%20%20-%20**Late%20enrollees%20are%20censored%2C%20not%20zeroed.**%20A%20unit%20whose%20window%2Fband%0A%20%20%20%20%20%20has%20not%20had%20time%20to%20close%20by%20%60observation_end%60%20(explicit%2C%20or%20for%20a%0A%20%20%20%20%20%20running%20experiment%2C%20the%20latest%20date%20that%20metric's%20own%20column%20was%0A%20%20%20%20%20%20actually%20observed)%20is%20dropped%20entirely%20rather%20than%20read%20as%20%22observed%2C%0A%20%20%20%20%20%20did%20not%20return.%22%20A%20%60UserWarning%60%20fires%20if%20this%20drops%20more%20than%2010%25%20of%0A%20%20%20%20%20%20enrolled%20units.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Analysis%2C%20MetricSpec%2C%20estimates_to_readout%2C%20panel_df%2C%20readout_table)%3A%0A%20%20%20%20%23%20Every%20unit%20in%20this%20synthetic%20panel%20enrolled%20on%20the%20same%20day%3B%20a%20real%0A%20%20%20%20%23%20dataset%20would%20carry%20its%20own%20per-unit%20exposure_date%20column%20instead.%0A%20%20%20%20panel_df_exposed%20%3D%20panel_df.assign(exposure_date%3D%222026-02-01%22)%0A%0A%20%20%20%20windowed_lift_breakout%20%3D%20Analysis.from_unit_panel(%0A%20%20%20%20%20%20%20%20panel_df_exposed%2C%0A%20%20%20%20%20%20%20%20unit%3D%22user_id%22%2C%0A%20%20%20%20%20%20%20%20group%3D%22variant%22%2C%0A%20%20%20%20%20%20%20%20date%3D%22day%22%2C%0A%20%20%20%20%20%20%20%20control%3D%22control%22%2C%0A%20%20%20%20%20%20%20%20metrics%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20MetricSpec(name%3D%22revenue%22%2C%20type%3D%22mean%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20MetricSpec(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22revenue%20(3d%20window)%22%2C%20value_column%3D%22revenue%22%2C%20type%3D%22mean%22%2C%20window_days%3D3%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20exposure_date%3D%22exposure_date%22%2C%0A%20%20%20%20).run()%0A%0A%20%20%20%20readout_table(%0A%20%20%20%20%20%20%20%20estimates_to_readout(windowed_lift_breakout)%2C%0A%20%20%20%20%20%20%20%20title%3D%22Windowed%20Lift%22%2C%0A%20%20%20%20%20%20%20%20%23%20nest_by%3D'method'%0A%20%20%20%20)%0A%20%20%20%20return%20(panel_df_exposed%2C)%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20Analysis%2C%0A%20%20%20%20MetricSpec%2C%0A%20%20%20%20estimates_to_readout%2C%0A%20%20%20%20panel_df_exposed%2C%0A%20%20%20%20readout_table%2C%0A)%3A%0A%20%20%20%20retention_lift%20%3D%20Analysis.from_unit_panel(%0A%20%20%20%20%20%20%20%20panel_df_exposed%2C%0A%20%20%20%20%20%20%20%20unit%3D%22user_id%22%2C%0A%20%20%20%20%20%20%20%20group%3D%22variant%22%2C%0A%20%20%20%20%20%20%20%20date%3D%22day%22%2C%0A%20%20%20%20%20%20%20%20control%3D%22control%22%2C%0A%20%20%20%20%20%20%20%20metrics%3D%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20MetricSpec(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22returned%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20type%3D%22retention%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20value_column%3D%22revenue%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20threshold_days%3D(1%2C%205)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20exposure_date%3D%22exposure_date%22%2C%0A%20%20%20%20).run()%0A%0A%20%20%20%20readout_table(%0A%20%20%20%20%20%20%20%20estimates_to_readout(retention_lift)%2C%0A%20%20%20%20%20%20%20%20title%3D%22Retention%20Lift%22%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20What's%20still%20not%20here%0A%0A%20%20%20%20%60from_unit_panel%60%20covariates%20(CUPED)%20remain%20unsupported%20-%20collapsing%20a%0A%20%20%20%20pre-period%20value%20from%20per-day%20rows%20is%20ambiguous%3B%20aggregate%20to%20one%20row%0A%20%20%20%20per%20unit%20yourself%20and%20use%20%60from_unit_summary%60%20instead%20(see%0A%20%20%20%20%5B%60docs%2Fguides%2Fcuped.md%60%5D(..%2Fdocs%2Fguides%2Fcuped.md)).%0A%0A%20%20%20%20%60Analysis.from_definitions%60%20remains%20the%20richer%20retention%20home%3A%20a%0A%20%20%20%20bounded%20%60RetentionMetric%60%20supports%20all%20five%20readout%20axes%20there.%0A%20%20%20%20%60from_unit_panel%60%20retention%20supports%20%60run()%60%2C%20%60run_asof()%60%2C%20and%0A%20%20%20%20%60run_asof_lift()%60%2C%20but%20raises%20%60CapabilityError%60%20from%20%60run_daily()%60%0A%20%20%20%20and%20%60run_daily_lift()%60%20-%20the%20frame-backed%20panel%20has%20no%20per-day%0A%20%20%20%20cohort%20reduction%2C%20bounded%20band%20or%20not.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
82cac1455f66e820b4c404f9f83890d0