Metric cards home-v2-active-days

Home v2 active-days lift

Treatment-control difference in mean active days after assignment

1 The gist · for everyone

Did users assigned to Home v2 have more active app days after assignment than users assigned to control?

+0.21 days
more per user
Plausible range +0.05 days to +0.37 days

Among users kept by the saved analysis, users had 0.21 more active days than users in days 1-28 after they were assigned.

Control users averaged 6.42 active days; treatment users averaged 6.63.

For every 100 users in the analysis table, Home v2 adds about 21 active user-days over the 28-day follow-up window.

What kind of claim: Shows a link. Groups are compared, but nothing rules out other differences between them. Treat it as a link, not a cause.
0 days2 days4 days6 daysHome v26.63 days · n=41,602Control6.42 days · n=41,880Difference vs Control: +0.21 days
Subtract the control average from the treatment average.

Safe to say

"In the saved analysis, treatment users averaged 0.21 more active days than control users over days 1-28 after assignment."

Not this

"Home v2 caused all new users to be active 0.21 more days per month."

Before you repeat it

The query is not pure : it keeps only users joined to an , excludes China, and drops users with zero recorded activity days in the outcome window.

2 How it's built · for the curious

How the number is made

Follow one user: Alex

Alex is assigned to on 10 May 2026, sees the variant screen, is outside China, and has app activity on several later dates.

Assignment window · days 0–13Days 1-28 after assignment · days 1–28Assignment date (day 0)12345day 13day 28
  1. 1day 0 Assigned to Home v2 and sees the variant screen
  2. 2day 0 Uses the app on assignment day; this date does not count
  3. 3day 1 Uses the app the next day counts
  4. 4day 28 Uses the app on day 28 counts
  5. 5day 29 Uses the app on day 29; outside the window

Result: Alex contributes 2 to the average. Assignment-day activity and day-29 activity do not count.

Now the general rule, one step at a time. Each step has the plain version, with the exact version one click away.

  1. Step 1 · Who is counted

    Start with users, then apply each rule

    One app user in the Home v2 experiment analysis table.

    Exactly

    One user_id from exp.assignments for experiment_id = 'home_v2', after joining to exp.exposures with saw_variant_screen = 1, excluding country_code = 'CN', and requiring at least one joined app.events activity_date in the outcome window.

    S3
    All users
    Keep if Assigned to the Home v2 experiment from 4 May through 17 May 2026.
    Keep if Saw the variant screen and was not in China.
    Keep if Had at least one recorded activity day in days 1-28 after assignment.83,482 remain
    Counted in the denominator
  2. Step 2 · When the clock starts

    Time zero: assignment date

    Each user's clock starts on their assignment date.

    • Assignment window: The experiment admitted users assigned from 4 May through 17 May 2026. (days 0–13)
    • Days 1-28 after assignment: The 28 calendar dates after the assignment date, excluding assignment day. (days 1–28)
    Exactly

    assigned_date = CAST(a.assigned_at AS DATE).

    S3
    Assignment window · days 0–13Days 1-28 after assignment · days 1–28Assignment date (day 0)day 13day 28
  3. Step 3 · What counts

    Active days

    Counts how many different dates had any app activity in the outcome window.

    Exactly

    COUNT(DISTINCT act.activity_date), where activity_date is DATE(e.event_ts) from app.events grouped by user_id and UTC-derived activity_date.

    S3
    Assignment window · days 0–13Days 1-28 after assignment · days 1–28Assignment date (day 0)12345day 13day 28
  4. Step 4 · Combine

    Average across people

    Average the active-day counts within each experiment arm.

    Exactly

    AVG(active_days) grouped by arm; denominator is COUNT(*) from per_user.

    S3
    sum of per-user active_days
    users retained in each arm's per_user table41,602
    = 6.63 daysHome v2
  5. Step 5 · Compare

    Take the difference

    Subtract the average from the treatment average.

    mean_active_days_treatment - mean_active_days_control

    With the real numbers: 6.63 − 6.42 = 0.210

    Whose effect? No causal target; a comparison of observed numbers.

    Exactly

    diff = treatment.mean() - control.mean(); from the summary table, 6.63 - 6.42 = 0.21 days.

    S4S5
    0 days2 days4 days6 daysHome v26.63 days · n=41,602Control6.42 days · n=41,880Difference vs Control: +0.21 days
  6. Step 6 · How sure

    Show the range of plausible values

    The saved interval uses the spread of per-user active days in each arm to estimate sampling uncertainty around the treatment-control difference.

    Not covered by this range: The interval does not cover bias from post-assignment filtering, UTC-vs-local date mismatch, missing events, or whether zero-activity assigned users were dropped.

    Exactly

    se = sqrt(var_treatment / n_treatment + var_control / n_control); interval = diff +/- 1.96 * se. p comes from scipy.stats.ttest_ind(..., equal_var=False).

    S4S5
    0.00 days+0.20 days+0.40 daysno difference
    Each dot is one of 20 equally likely values, given sampling noise alone (normal approximation to the interval).

3 Keep in mind · before you act on it

What could make this misleading

Complete. The latest assignment date is 17 May 2026; its days 1-28 after assignment ends 14 Jun 2026. Data are complete through 20 Jun 2026, so every window is fully observed. This number should not move as more data arrive.

Caveats

↑ May make the number look bigger than it really is↓ May make the number look smaller than it really is± The true uncertainty is larger than shown↕ Could push the number either way◎ Limits what the number can be used to conclude
◎

Not pure intent-to-treat

The readout says every assigned user is analyzed in their assigned arm, but the query keeps only users with a matching and at least one post-assignment activity day. If changes exposure or whether a user has any activity at all, the reported mean is conditional on a post-randomization outcome.

Limits what the number can be used to conclude · high importance S1 S3

↕

Zero-activity users are not in the denominator

Because per_user uses an inner join to activity, users with no recorded in days 1-28 are absent rather than counted as zero active days. This likely overstates each arm's average active days; the direction for the treatment-control difference is unknown.

Could push the number either way · high importance S3

↕

Time zone mismatch

The readout says activity is counted in each user's local time zone. The query turns event times into dates before counting active days.

Could push the number either way · medium importance S1 S3

◎

Scope is narrower than all new users

The implemented population excludes China and requires seeing the variant screen. The saved result should not be generalized to all new users without checking those exclusions.

Limits what the number can be used to conclude · medium importance S2 S3

Common misreadings

Tempting, but wrongAccurate
Home v2 caused all new users to be active 0.21 more days per month.The saved analysis shows a 0.21-day difference among exposed, non-CN users retained in the per_user table over days 1-28 after assignment.
0.21 days is a 0.21% lift.It is an absolute difference in days. Relative to the 6.42-day average, it is about a 3.3% lift.
The denominator is all assigned users.The SQL denominator is users in per_user, after the exposure join, China exclusion, and inner join to activity.
Days 1-28 are local-calendar days.The SQL derives dates with DATE(event_ts), and the source comment says event_ts is stored in UTC.

Judgment calls

What to treat as the headline

Chose: Use the 0.21-day treatment-control difference, because both readouts emphasize that number and the CSV stores the matching difference and interval.

Could have chosen: A card could instead headline the mean (6.63 days), the (+3.3%), or an rerun if zero-activity assigned users were available.

Why: The user asked for the headline number readers see, and the readout repeats +0.21 days.

What it changes: The card focuses on the difference in means and treats the relative lift as a secondary restatement.

How to state causality

Chose: Mark the saved result as associational, even though the experiment assignment was randomized.

Could have chosen: If the analysis were rerun on all assigned users as true intent-to-treat, the claim strength could be causal for the randomized population.

Why: The implemented SQL applies post-assignment exposure and activity filters, so the analyzed denominator may no longer preserve the original randomization.

What it changes: This prevents the card from repeating the readout's broader causal claim until the denominator is verified or rerun.

4 Reproduce it · for specialists

The exact specification

Estimand

PopulationUsers assigned to home_v2 from 2026-05-04 through 2026-05-17, excluding China, joined to a saw_variant_screen exposure row, and present in app.events on at least one date in days 1-28.
Exposure / contrastHome v2 treatment assignment versus control assignment, as labeled by arm.
Time zeroCAST(assigned_at AS DATE) for each user.
VariableCount of distinct UTC-derived app activity dates greater than assigned_date and less than or equal to assigned_date plus 28 days.
Intercurrent eventsThe saved SQL does not specify special handling for uninstall, churn, travel, or missing events. Users with no activity in the window are excluded by the inner join rather than retained with zero.
Summary measureDifference in arm-level means, treatment minus control, with a Welch t-test confidence interval.
Targetdescriptive: No causal target; a comparison of observed numbers

Where the description and the code disagree

Intent-to-treat denominator

Described

README says every assigned user is analyzed in the arm they were assigned to.

Implemented

assigned joins to exposures with saw_variant_screen = 1, then per_user inner joins to activity, excluding assigned users without a matching exposure row or without activity in days 1-28.

Impact: Narrows the denominator from all assigned users to exposed active users. The impact on the +0.21-day difference is unknown from saved outputs.

S1S3

Geographic scope

Described

The readouts describe randomized users or all new users without a country exclusion.

Implemented

The SQL filters a.country_code <> 'CN'.

Impact: Results do not cover China. Direction and size of any difference are unknown.

S2S3

Time zone for active dates

Described

README says active days use the user's local time zone.

Implemented

activity_date is DATE(e.event_ts), and the SQL comment says event_ts is stored in UTC.

Impact: Activity near local midnight can land on a different date. The effect on distinct active-day counts is unknown.

S1S3

Breadth of causal claim

Described

Readout says Home v2 causes users to be active 0.21 more days a month and applies to all new users.

Implemented

The code estimates a treatment-control difference over days 1-28 among exposed, non-CN users who had at least one activity day in the outcome window.

Impact: The implemented result is narrower than the readout claim. A true all-assigned ITT estimate could be smaller or larger.

S2S3

Rules as implemented

StageExact ruleSource
Eligibility 1a.experiment_id = 'home_v2' and assigned_at >= TIMESTAMP '2026-05-04 00:00:00' and assigned_at < TIMESTAMP '2026-05-18 00:00:00'.S3
Eligibility 2INNER JOIN exp.exposures on user_id and experiment_id with saw_variant_screen = 1; country_code <> 'CN'.S3
Eligibility 3
83,482 remain
per_user is built with an INNER JOIN from assigned to activity on act.activity_date > assigned_date and act.activity_date <= DATE_ADD(assigned_date, 28), so users with no activity_date in that interval are not in the final COUNT(*).S3S4
Time zeroassigned_date = CAST(a.assigned_at AS DATE).S3
Assignment window
[0, 14) days
assigned_at >= 2026-05-04 00:00:00 and assigned_at < 2026-05-18 00:00:00.S3
Days 1-28 after assignment
[1, 29) days
act.activity_date > assigned_date AND act.activity_date <= DATE_ADD(assigned_date, 28).S3
Active days
count
COUNT(DISTINCT act.activity_date), where activity_date is DATE(e.event_ts) from app.events grouped by user_id and UTC-derived activity_date.S3
AggregateAVG(active_days) grouped by arm; denominator is COUNT(*) from per_user.S3
Comparisondiff = treatment.mean() - control.mean(); from the summary table, 6.63 - 6.42 = 0.21 days.S4S5
Uncertaintyse = sqrt(var_treatment / n_treatment + var_control / n_control); interval = diff +/- 1.96 * se. p comes from scipy.stats.ttest_ind(..., equal_var=False).S4S5

Numbers per group

GroupDefinitionValuen
Controlarm = 'control' in the per_user export.6.4241,880
Home v2arm = 'treatment' in the per_user export.6.6341,602

Check: 6.63 − 6.42 = 0.210 (headline 0.21).

Parameters

NameValueMeaning
experiment_idhome_v2Experiment analyzed
assignment_start2026-05-04 00:00:00Inclusive assignment start
assignment_end2026-05-18 00:00:00Exclusive assignment end
outcome_start1First included day after assignment
outcome_end28Last included day after assignment
excluded_countryCNCountry excluded in SQL

Data lineage

  1. exp.assignments

    Experiment assignment and arm labels

    Grain: user_id x experiment assignment · Filters: experiment_id = home_v2; assigned_at in [2026-05-04, 2026-05-18); country_code <> CN

  2. exp.exposures

    Exposure filter

    Grain: user_id x experiment exposure · Filters: same experiment_id; saw_variant_screen = 1

  3. app.events

    Daily app activity

    Grain: event · Filters: grouped to one activity_date per user_id using DATE(event_ts)

  4. results/ab_summary.csv

    Saved arm summaries, difference, interval, p-value, and data completeness date

    Grain: arm summary plus estimate rows

Code that defines it

Implemented population filterseval/fixtures/syn-b/sql/experiment_metric.sql:1-30
FROM exp.assignments a
JOIN exp.exposures x
  ON x.user_id = a.user_id
 AND x.experiment_id = a.experiment_id
 AND x.saw_variant_screen = 1
WHERE a.experiment_id = 'home_v2'
  AND a.assigned_at >= TIMESTAMP '2026-05-04 00:00:00'
  AND a.assigned_at <  TIMESTAMP '2026-05-18 00:00:00'
  AND a.country_code <> 'CN'
Active-day outcome windoweval/fixtures/syn-b/sql/experiment_metric.sql:1-30
SELECT e.user_id, DATE(e.event_ts) AS activity_date      -- event_ts is stored in UTC
FROM app.events e
GROUP BY e.user_id, DATE(e.event_ts)
...
AND act.activity_date >  s.assigned_date
AND act.activity_date <= DATE_ADD(s.assigned_date, 28)
Difference and uncertaintyeval/fixtures/syn-b/src/analysis.py:1-9
diff = treatment.mean() - control.mean()
se = np.sqrt(treatment.var(ddof=1) / len(treatment) + control.var(ddof=1) / len(control))
t, p = stats.ttest_ind(treatment, control, equal_var=False)
return {"diff": diff, "ci_low": diff - 1.96 * se, "ci_high": diff + 1.96 * se, "p": p}

Rebuild it

Data complete through: 2026-06-20

Receipts:

  • CSV arithmetic check: treatment mean 6.63 - control mean 6.42 = +0.21 active days.
  • Saved rows report n = 41,880 control and 41,602 treatment, total n = 83,482.
  • Saved interval is 0.05 to 0.37 with p = 0.011 and data_complete_through = 2026-06-20.

Tests: No executable test files were present in the fixture. The card was linted and rendered with the mcard tooling.

Sources

  1. S1 eval/fixtures/syn-b/README.md lines 1-15 · Experiment design, stated ITT, primary metric definition, and reported result
  2. S2 eval/fixtures/syn-b/readout.md lines 1-4 · Headline leadership readout
  3. S3 eval/fixtures/syn-b/sql/experiment_metric.sql lines 1-30 · Implemented population, activity-date derivation, outcome window, and aggregation
  4. S4 eval/fixtures/syn-b/results/ab_summary.csv lines 1-8 · Saved group means, sample sizes, interval, p-value, and data completeness date
  5. S5 eval/fixtures/syn-b/src/analysis.py lines 1-9 · Welch t-test calculation

Change log

  • Last assignment date included in the experiment window.
  • Saved result data complete through this date.
  • Metric card written from fixture syn-b; four description-code discrepancies recorded.
Card quality checks: 0 errors · 0 warnings
  • info recompute estimate reproduced from cells: 6.63 − 6.42 = 0.210

Check yourself

Could you explain it to a colleague?

Big pictureWhat is the safest reading of the +0.21 headline?
DetailWhich users are missing from the implemented denominator?
Apply itA user is assigned on 10 May, uses the app on 10 May, 11 May, and 8 June. Which dates count?

Explain it back

Write one or two sentences as if telling a colleague. Then compare with the checklist.

Glossary

95% confidence interval (95% CI, confidence interval, CI)
A range of effect sizes that fit the saved data under the statistical model. It does not include every possible source of bias.
active day (active days)
A calendar date with at least one recorded app activity event.
One distinct DATE(event_ts) in app.events after grouping by user_id.
control
The comparison arm that did not get Home v2.
exposure row (exposure)
A record that the user saw the variant screen for the experiment.
Home v2
The treatment version of the app home screen in this experiment.
intent-to-treat (ITT)
An experiment analysis that keeps everyone in the group they were assigned to, whether or not they fully saw or used the treatment.
relative lift
The difference divided by the comparison value. Here, 0.21 extra days divided by 6.42 control days is about 3.3%.
treatment (treatment users)
The arm assigned to Home v2.
UTC
A global time standard. If dates are cut in UTC instead of a user's local time, events near midnight can fall on a different date.
Welch t-test (Welch test)
A statistical comparison of two averages that allows the two groups to have different spreads.